Abschlußprojekt für Data Analyst: Entwicklung eines Autopreisvorhersagemodells und Integration in ein Dashboard zur Anwendung.¶
Autor Guangfa Wu, 18.08.2025, Stuttgart¶
Daten: https://www.kaggle.com/datasets/hellbuoy/car-price-prediction¶
Projektidee, Beschreibung:¶
Problembeschreibung: Autopreis vorhersagen¶
Ein chinesisches Automobilunternehmen, Geely Auto, plant den Eintritt in den US-amerikanischen Markt, indem es dort eine Produktionsstätte errichtet und Autos lokal herstellt, um mit amerikanischen und europäischen Konkurrenten zu konkurrieren. Dazu hat Geely ein Beratungsunternehmen für die Automobilbranche beauftragt, um die Faktoren zu analysieren, die den Preis von Autos beeinflussen. Insbesondere möchten sie verstehen, welche Faktoren den Preis von Autos auf dem amerikanischen Markt bestimmen, da diese sich deutlich vom chinesischen Markt unterscheiden könnten.
Das Unternehmen möchte wissen:¶
- Welche Variablen sind signifikant für die Vorhersage des Autopreises?
- Wie gut erklären diese Variablen den Preis eines Autos?
Vorgegensweise:¶
CRISP-DM wird angewendet¶
Basierend auf verschiedenen Marktforschungen hat das Beratungsunternehmen einen umfangreichen Datensatz mit unterschiedlichen Fahrzeugtypen auf dem US-amerikanischen Markt zusammengetragen. Lösung der Aufgabestellung:
- Ein Modell soll entwickelt werden, das den Preis von Autos basierend auf den verfügbaren unabhängigen Variablen am besten vorhersagt. Da ein Auto aus verschieden Komponenten besteht, ist es nur sinnvoll, alle Komponenten zusammen zu betrachten und ein Preismodell zu erstellen.
- Gleichzeitig kann auch untersucht werden welche einzelnen Faktoren den Preis beeinflussen.
- Es wird ein Dashboard für Modelltraining durch Auswahl von beteiligten Spaten erstellt. Der Anwender kann selbst die Spalten auswählen und das Modell trinieren und das Ergebnis anzeigen und bewerten.
Anwendung:¶
Das Modell soll dem Management dabei helfen zu verstehen:¶
- Wie sich der Preis je nach Veränderung einzelner Merkmale verändert
- Welche Stellschrauben (Design, Strategie etc.) genutzt werden können, um bestimmte Preisniveaus zu erreichen.
- Zusätzlich ist das Modell ein hilfreiches Werkzeug, um die Preisdynamik in einem neuen Markt wie den USA zu verstehen.
- Es wird ein Dashboard erstellt. der Anwender kann selbst die einzelnen Parameter auswählen und den Preis abzuschätzen
Der bereitgestellte Datensatz dient ausschließlich zu Lernzwecken. Ziehen keine Rückschlüsse auf reale Märkte oder tatsächliche Marktbedingungen.¶
Die Arbeitsschritte richten sich nach nach CRISP-DM¶
1. Business Understanding (Aufgabendefinition)¶
Fahrzeugsegmente und Preisspannen¶
Das Preissegment für Autos wird üblicherweise in verschiedene Kategorien eingeteilt,
die sich an der Größe, Ausstattung und Komfort des Fahrzeugs orientieren.
Oft werden diese Segmente mit Buchstaben (A, B, C, D, …) bezeichnet und unterscheiden sich durch typische Preisspannen.
Hinweis: Die Preisbereiche sind Richtwerte und können je nach Marke, Modell, Ausstattung und Region variieren.
🚗 Übersicht gängiger Fahrzeugsegmente¶
| Fahrzeugsegment | Beschreibung | Typische Preisspanne (Neuwagen) | Bemerkungen zu Kosten |
|---|---|---|---|
| A-Segment | Kleinstwagen, sehr kompakt, ideal für Stadtverkehr | unter 15.000 € | Günstig in Anschaffung und Unterhalt |
| B-Segment | Kleinwagen, etwas größer, alltagstauglicher, mehr Komfort | 15.000 – 25.000 € | Kompromiss zwischen Preis und Platz/Komfort |
| C-Segment | Kompaktklasse, familienfreundlich, gute Mischung aus Platz und Ausstattung | 20.000 – 35.000 € | Vielseitig, gute Ausstattung |
| D-Segment | Mittelklasse, komfortabler, bessere Ausstattung | ab ca. 35.000 € | Höhere Anschaffungskosten |
| Oberklasse / Luxus | Große und luxuriöse Fahrzeuge | ab ca. 50.000 € | Teuer, höherer Unterhalt, Luxusausstattung |
📌 Relevanz für die Analyse¶
In der Aufgabenstellung gibt der Hersteller keine Informationen, auf welches Preissegment er sich fokussieren möchte.
Als Data Analyst ist es daher notwendig:
- Mit dem Auftraggeber zu kommunizieren, um die Anforderungen und Geschäftsziele genau zu verstehen.
- Klarzustellen, ob die Vorhersage für den Gesamtmarkt oder für ein bestimmtes Preissegment erfolgen soll.
- Auf Basis der gewählten Segmente die Preisvorhersagen gezielt an die jeweilige Kategorie anzupassen.
Datenquelle:¶
CarPrice_Assignment.csv¶
Information von Auto: 26 Spalten¶
- car_ID: Identification Number for Each Car
- symbling: Car's Safety Rating (-3 (sehr sicher) bis +3 (hohes Risiko).
- CarName: Name of the Car Model
- fueltype: Type of Fuel Used (Gasoline, Diesel, Electric, etc.)
- aspiration: Type of Aspiration (Standard or Turbocharged)
- doornumber: Number of Doors on the Car
- carbody: Style of the Car's Body (Sedan, Coupe, SUV, etc.)
- drivewheel: Type of Drive Wheels (Front, Rear, All)
- enginelocation: Location of the Car's Engine (Front or Rear)
- wheelbase: Length of the Car's Wheelbase
- carlength: Overall Length of the Car
- carwidth: Width of the Car
- carheight: Height of the Car
- curbweight: Weight of the Car without Passengers or Cargo
- enginetype: Type of Engine (Gas, Diesel, Electric, etc.)
- cylindernumber: Number of Cylinders in the Engine
- enginesize: Size of the Car's Engine
- fuelsystem: Type of Fuel Delivery System
- boreratio: Bore-to-Stroke Ratio of the Engine
- stroke: Stroke Length of the Engine
- compressionratio: Compression Ratio of the Engine
- horsepower: Car's Engine Horsepower
- peakrpm: Engine's Peak RPM (Revolutions Per Minute)
- citympg: Miles Per Gallon (MPG) in City Driving
- highwaympg: MPG on the Highway
- price: Price of the Car
# import packages
import pandas as pd
import numpy as np
import json
import math
import matplotlib
from datetime import timedelta, datetime
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
from matplotlib.pyplot import figure
from datetime import date
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
plt.style.use('seaborn-v0_8-notebook')
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = (12,8)
pd.options.mode.chained_assignment = None
import seaborn as sns
Einlesenvon Daten aus der csv-Datei:¶
# Einlesen von Daten
df = pd.read_csv("CarPrice_Assignment.csv")
door_mapCt=0 # um mehrfach map zu vermeiden
door_mapCt1=0 # um mehrfach map zu vermeiden
pd.set_option('display.max_columns', None) # zeigt alle Spalten an
2. Data Understanding (Auswahl der relevanten Datenbestände)¶
2.1 Deskriptive Analyse¶
Bei der deskriptiven Analyse werden die Daten mithilfe einfacher statistischer Methoden untersucht, um einen Überblick über die Datenstruktur, Verteilungen und Zusammenhänge zu erhalten.
Das Ziel ist es, die Eigenschaften der Daten zu beschreiben, z. B.:
- Mittelwerte
- Streuung (Standardabweichung, Varianz)
- Häufigkeiten
- Verteilungen
👉 Wichtig: Es geht nicht um Vorhersagen oder Ursache-Wirkungs-Beziehungen, sondern um ein erstes Verständnis der Daten.
🔎 Erste Methoden für einen Überblick¶
df.head()→ zeigt die ersten Zeilen der Datendf.info()→ Überblick über Datentypen und fehlende Wertedf.describe()→ zentrale statistische Kennzahlen (Mittelwert, Min, Max, Quartile)df.nunique()→ Anzahl der eindeutigen Werte pro Spalte- Wertverteilungen mit
value_counts()für kategoriale Spalten
Diese Methoden liefern schnell ein klares Bild über die Struktur und Qualität der Daten.
| Methode | Zweck / Was sie zeigt | Warum sinnvoll für Data Analysts |
|---|---|---|
df.info() |
Gibt Spaltennamen, Datentypen, Anzahl nicht fehlender Werte und Speicherverbrauch aus | Schneller Überblick über Datentypen, fehlende Werte und Speicherbedarf. Hilft bei der Datenvorbereitung. |
df.describe() |
Statistische Kennzahlen numerischer Spalten: count, mean, std, min, 25%, 50%, 75%, max | Erkennt Verteilungen, Ausreißer und zentrale Tendenzen. Grundlage für Transformationen und Feature Engineering. |
df.nunique() |
Anzahl der eindeutigen Werte pro Spalte | Identifikation von kategorialen Spalten, Variabilität von Features, Erkennen von konstanten oder redundanten Spalten. |
df.isna().sum() |
Anzahl der fehlenden Werte (NaN) pro Spalte |
Hilft, Spalten mit fehlenden Daten schnell zu identifizieren, Planung von Imputation oder Bereinigung der Daten. |
# Ein Überblick über die Daten mit df.head()
df.head()
| car_ID | symboling | CarName | fueltype | aspiration | doornumber | carbody | drivewheel | enginelocation | wheelbase | carlength | carwidth | carheight | curbweight | enginetype | cylindernumber | enginesize | fuelsystem | boreratio | stroke | compressionratio | horsepower | peakrpm | citympg | highwaympg | price | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 3 | alfa-romero giulia | gas | std | two | convertible | rwd | front | 88.6 | 168.8 | 64.1 | 48.8 | 2548 | dohc | four | 130 | mpfi | 3.47 | 2.68 | 9.0 | 111 | 5000 | 21 | 27 | 13495.0 |
| 1 | 2 | 3 | alfa-romero stelvio | gas | std | two | convertible | rwd | front | 88.6 | 168.8 | 64.1 | 48.8 | 2548 | dohc | four | 130 | mpfi | 3.47 | 2.68 | 9.0 | 111 | 5000 | 21 | 27 | 16500.0 |
| 2 | 3 | 1 | alfa-romero Quadrifoglio | gas | std | two | hatchback | rwd | front | 94.5 | 171.2 | 65.5 | 52.4 | 2823 | ohcv | six | 152 | mpfi | 2.68 | 3.47 | 9.0 | 154 | 5000 | 19 | 26 | 16500.0 |
| 3 | 4 | 2 | audi 100 ls | gas | std | four | sedan | fwd | front | 99.8 | 176.6 | 66.2 | 54.3 | 2337 | ohc | four | 109 | mpfi | 3.19 | 3.40 | 10.0 | 102 | 5500 | 24 | 30 | 13950.0 |
| 4 | 5 | 2 | audi 100ls | gas | std | four | sedan | 4wd | front | 99.4 | 176.6 | 66.4 | 54.3 | 2824 | ohc | five | 136 | mpfi | 3.19 | 3.40 | 8.0 | 115 | 5500 | 18 | 22 | 17450.0 |
2.1.1 Die Methode df.info() in Pandas¶
Die Methode df.info() gibt einen kompakten Überblick über den DataFrame.
Sie zeigt für jede Spalte:
- Spaltenname
- Anzahl der nicht fehlenden Werte (Non-Null Count)
- Datentyp (
dtype) - Speicherverbrauch des DataFrames
📌 Ergebnisse für die Projektdaten¶
- Es gibt 26 Spalten und 205 Zeilen.
- Es gibt keine fehlenden Werte (NaN).
- Die Spalten
doornumberundcylindernumberwerden alsobject(Text) dargestellt.- Dies sollte untersucht werden, da sie eigentlich numerische Werte (Anzahl der Türen bzw. Zylinder) enthalten sollten.
- Möglicher Grund: Werte könnten als Strings gespeichert sein (z. B.
"four"statt4).
# zeige die kompakte Information über die Daten
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 205 entries, 0 to 204 Data columns (total 26 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 car_ID 205 non-null int64 1 symboling 205 non-null int64 2 CarName 205 non-null object 3 fueltype 205 non-null object 4 aspiration 205 non-null object 5 doornumber 205 non-null object 6 carbody 205 non-null object 7 drivewheel 205 non-null object 8 enginelocation 205 non-null object 9 wheelbase 205 non-null float64 10 carlength 205 non-null float64 11 carwidth 205 non-null float64 12 carheight 205 non-null float64 13 curbweight 205 non-null int64 14 enginetype 205 non-null object 15 cylindernumber 205 non-null object 16 enginesize 205 non-null int64 17 fuelsystem 205 non-null object 18 boreratio 205 non-null float64 19 stroke 205 non-null float64 20 compressionratio 205 non-null float64 21 horsepower 205 non-null int64 22 peakrpm 205 non-null int64 23 citympg 205 non-null int64 24 highwaympg 205 non-null int64 25 price 205 non-null float64 dtypes: float64(8), int64(8), object(10) memory usage: 41.8+ KB
2.1.2 Check NAN: Ergebnis:keine NaN vorhanden. Die Behandlung von NaN entfällt.¶
# Prüfen wie viele Nan jeder Spalte gibt: zwar df.info() hat gezeigt es keine NAN gibt. es ist üblich df.isna().sum() auch mal zu benutzen
# df.isnull().sum()
df.isna().sum()
car_ID 0 symboling 0 CarName 0 fueltype 0 aspiration 0 doornumber 0 carbody 0 drivewheel 0 enginelocation 0 wheelbase 0 carlength 0 carwidth 0 carheight 0 curbweight 0 enginetype 0 cylindernumber 0 enginesize 0 fuelsystem 0 boreratio 0 stroke 0 compressionratio 0 horsepower 0 peakrpm 0 citympg 0 highwaympg 0 price 0 dtype: int64
2.1.3 Untersuchung von cylindernumber und doornumber: sind als Kategorie angegeben, in Zahl umwandeln¶
- Es ist anzunehmen, dass vier Tür mehr als zwei Tür kostet. ['two' 'four'] in Zahlen: 2, 4
- Es ist anzunehmen, dass der Preis mit der Anzahl der Zylinder steigt. ['four', 'six', 'five', 'three', 'twelve', 'two', 'eight']: z.B. 'two' in 2, 'twelve' in 12 umwandeln: 2,3,4,5,6,8,12
# zeige welche Anzahl von Tür ein Auto hat. z.B. 2,3,5
# zeige auch die Anzahl von Autos von 2 und 4 Türen
print(df.doornumber.unique())
print(df.groupby(['doornumber']).count())
df.groupby(['doornumber']).mean(numeric_only=True)
['two' 'four']
car_ID symboling CarName fueltype aspiration carbody \
doornumber
four 115 115 115 115 115 115
two 90 90 90 90 90 90
drivewheel enginelocation wheelbase carlength carwidth \
doornumber
four 115 115 115 115 115
two 90 90 90 90 90
carheight curbweight enginetype cylindernumber enginesize \
doornumber
four 115 115 115 115 115
two 90 90 90 90 90
fuelsystem boreratio stroke compressionratio horsepower \
doornumber
four 115 115 115 115 115
two 90 90 90 90 90
peakrpm citympg highwaympg price
doornumber
four 115 115 115 115
two 90 90 90 90
| car_ID | symboling | wheelbase | carlength | carwidth | carheight | curbweight | enginesize | boreratio | stroke | compressionratio | horsepower | peakrpm | citympg | highwaympg | price | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| doornumber | ||||||||||||||||
| four | 112.965217 | 0.104348 | 101.133913 | 178.388696 | 66.300000 | 54.915652 | 2646.260870 | 127.669565 | 3.358261 | 3.252348 | 10.766087 | 99.686957 | 5020.869565 | 25.147826 | 30.530435 | 13501.152174 |
| two | 90.266667 | 1.766667 | 95.718889 | 168.504444 | 65.406667 | 52.203333 | 2439.677778 | 125.933333 | 3.293333 | 3.259333 | 9.345778 | 109.777778 | 5258.333333 | 25.311111 | 31.033333 | 12989.924078 |
# zeige welche Zylinder Kategorie es gibt
df.cylindernumber.unique()
array(['four', 'six', 'five', 'three', 'twelve', 'two', 'eight'],
dtype=object)
# wie schon erwähnt. es ist sinnvoll, Anzahl von Tür und Zylinder in Zahl umzuwandeln
map_list= {'two':2,'three':3,'four':4, 'five':5,'six':6,'eight':8,'twelve':12}
if( door_mapCt1==0): #verhindern nochmaliges Umwandeln
df['cylindernumber'] = df['cylindernumber'].map(map_list).astype(int)
df['doornumber'] = df['doornumber'].map(map_list).astype(int)
door_mapCt1=1
print(df.cylindernumber)
print(df.doornumber)
0 4
1 4
2 6
3 4
4 5
..
200 4
201 4
202 6
203 6
204 4
Name: cylindernumber, Length: 205, dtype: int64
0 2
1 2
2 2
3 4
4 4
..
200 4
201 4
202 4
203 4
204 4
Name: doornumber, Length: 205, dtype: int64
Anwendungvon df.describe()¶
Die Methode df.describe() in Pandas liefert eine statistische Zusammenfassung der numerischen Spalten eines DataFrames.
Standardmäßig zeigt sie folgende Kennzahlen:
- count: Anzahl der nicht fehlenden Werte
- mean: Mittelwert der Spalte
- std: Standardabweichung
- min: Minimalwert
- 25%, 50%, 75%: Quartile (25%, Median, 75%)
- max: Maximalwert
Warum ist df.describe() sinnvoll für Data Analysts?¶
1. Schneller Überblick über die Daten¶
Man sieht sofort Mittelwerte, Streuung, Minimal- und Maximalwerte.
2. Datenqualität prüfen¶
Wenn count für eine Spalte kleiner als die Anzahl der Zeilen ist, gibt es fehlende Werte (NaN oder None).
3. Vergleich zwischen Spalten¶
Beispiel: citympg und highwaympg
df.describe()ermöglicht es, schnell zu erkennen, welche Spalten stark variieren und welche konstant sind.- Man kann erste Korrelationen und Ähnlichkeiten zwischen Spalten identifizieren.
# zeige die statistische Zusammenfassung
df.describe()
| car_ID | symboling | doornumber | wheelbase | carlength | carwidth | carheight | curbweight | cylindernumber | enginesize | boreratio | stroke | compressionratio | horsepower | peakrpm | citympg | highwaympg | price | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 | 205.000000 |
| mean | 103.000000 | 0.834146 | 3.121951 | 98.756585 | 174.049268 | 65.907805 | 53.724878 | 2555.565854 | 4.380488 | 126.907317 | 3.329756 | 3.255415 | 10.142537 | 104.117073 | 5125.121951 | 25.219512 | 30.751220 | 13276.710571 |
| std | 59.322565 | 1.245307 | 0.994966 | 6.021776 | 12.337289 | 2.145204 | 2.443522 | 520.680204 | 1.080854 | 41.642693 | 0.270844 | 0.313597 | 3.972040 | 39.544167 | 476.985643 | 6.542142 | 6.886443 | 7988.852332 |
| min | 1.000000 | -2.000000 | 2.000000 | 86.600000 | 141.100000 | 60.300000 | 47.800000 | 1488.000000 | 2.000000 | 61.000000 | 2.540000 | 2.070000 | 7.000000 | 48.000000 | 4150.000000 | 13.000000 | 16.000000 | 5118.000000 |
| 25% | 52.000000 | 0.000000 | 2.000000 | 94.500000 | 166.300000 | 64.100000 | 52.000000 | 2145.000000 | 4.000000 | 97.000000 | 3.150000 | 3.110000 | 8.600000 | 70.000000 | 4800.000000 | 19.000000 | 25.000000 | 7788.000000 |
| 50% | 103.000000 | 1.000000 | 4.000000 | 97.000000 | 173.200000 | 65.500000 | 54.100000 | 2414.000000 | 4.000000 | 120.000000 | 3.310000 | 3.290000 | 9.000000 | 95.000000 | 5200.000000 | 24.000000 | 30.000000 | 10295.000000 |
| 75% | 154.000000 | 2.000000 | 4.000000 | 102.400000 | 183.100000 | 66.900000 | 55.500000 | 2935.000000 | 4.000000 | 141.000000 | 3.580000 | 3.410000 | 9.400000 | 116.000000 | 5500.000000 | 30.000000 | 34.000000 | 16503.000000 |
| max | 205.000000 | 3.000000 | 4.000000 | 120.900000 | 208.100000 | 72.300000 | 59.800000 | 4066.000000 | 12.000000 | 326.000000 | 3.940000 | 4.170000 | 23.000000 | 288.000000 | 6600.000000 | 49.000000 | 54.000000 | 45400.000000 |
Überblick über die Anzahl der eindeutigen Werte (unique values)¶
Die Methode df.nunique() in Pandas gibt die Anzahl der eindeutigen Werte jeder Spalte eines DataFrames zurück.
📌 Warum ist df.nunique() sinnvoll für die Datenanalyse?¶
Identifikation von kategorialen Spalten
- Spalten mit wenigen eindeutigen Werten sind oft kategorial (z. B. Geschlecht, Region).
- Erkennt Variabilität in numerischen Spalten → wenn eine numerische Spalte nur wenige eindeutige Werte hat, könnte sie diskret oder ein Dummy-Feature sein.
Datenbereinigung und Feature Engineering
- Hilft, redundante Spalten oder Spalten mit konstanten Werten zu erkennen.
- Unterstützt Entscheidungen für Encoding-Strategien, z. B. One-Hot-Encoding oder Ordinal-Encoding.
Schneller Überblick über die Datenstruktur
- Data Analysts bekommen sofort ein Gefühl dafür, wie divers die Werte in jeder Spalte sind,
ohne alle Daten einzeln prüfen zu müssen.
- Data Analysts bekommen sofort ein Gefühl dafür, wie divers die Werte in jeder Spalte sind,
# Überblick über die Anzahl der eindeutigen Werte
df.nunique()
car_ID 205 symboling 6 CarName 147 fueltype 2 aspiration 2 doornumber 2 carbody 5 drivewheel 3 enginelocation 2 wheelbase 53 carlength 75 carwidth 44 carheight 49 curbweight 171 enginetype 7 cylindernumber 7 enginesize 44 fuelsystem 8 boreratio 38 stroke 37 compressionratio 32 horsepower 59 peakrpm 23 citympg 29 highwaympg 30 price 189 dtype: int64
2.1.4 Erstellung von Histogrammen für numerische Werte¶
Ein Histogramm stellt die Häufigkeitsverteilung der Werte einer numerischen Spalte grafisch dar.
Die Werte werden in sogenannte Bins (Intervalle) unterteilt, und für jedes Intervall wird angezeigt, wie viele Datenpunkte darin liegen.
Mit Histogrammen kann man auf einen Blick erkennen:
- die Verteilung der Daten (gleichmäßig, schief, normalverteilt, etc.),
- das Vorhandensein von Ausreißern oder ungewöhnlichen Werten (Grenze: -3std < x < +3std),
- Symmetrie oder Asymmetrie der Datenverteilung.
Für Data Analysts ist dies sinnvoll, da Histogramme helfen, die Daten besser zu verstehen, die Wahl geeigneter Transformationen zu planen (z. B. log, sqrt) und Vorverarbeitungsmaßnahmen für Machine-Learning-Modelle abzuleiten.
2.1.4.1 Analyse der Histogramme der Fahrzeugmerkmale¶
Die bereitgestellten Histogramme geben einen Überblick über die Verteilung verschiedener numerischer Merkmale im Datensatz.
Aussage über die Verteilung:¶
- symboling
- Verteilung: Ungleichmäßig, mehrere Peaks; häufigste Werte bei 0 und 1, signifikante Anteile bei 2 und 3, geringere Anteile bei -1, -2 und 4.
- Interpretation: Könnte eine Risikobewertung oder Kategorisierung darstellen. Die Konzentration um 0 und 1 zeigt, dass die meisten Fahrzeuge in diesen Kategorien liegen, extreme Werte sind selten.
- doornumber
- Verteilung: Bimodal mit Spitzen bei 2 und 4.
- Interpretation: Die Mehrheit der Fahrzeuge hat entweder 2 oder 4 Türen; 4-türige Fahrzeuge sind etwas häufiger.
- cylindernumber
- Verteilung: Stark rechtsschief, dominanter Peak bei 4 Zylindern; kleinere Anteile bei 5, 6, 8 und sehr wenige bei 3, 10, 12.
- Interpretation: 4-Zylinder-Motoren sind am häufigsten, größere Motoren selten.
- wheelbase
- Verteilung: Nahezu glockenförmig, leicht rechtsschief, Hauptteil zwischen 90-110, Peak um 95-100.
- Interpretation: Die meisten Fahrzeuge haben einen mittleren Radstand.
- carlength
- Verteilung: Glockenförmig, leicht rechtsschief, meist zwischen 160-180, Peak um 170.
- Interpretation: Fahrzeuglängen konzentrieren sich auf mittlere Werte.
- carwidth
- Verteilung: Glockenförmig, leicht rechtsschief, Peak um 65.
- Interpretation: Fahrzeugbreiten liegen in einem engen Bereich.
- carheight
- Verteilung: Relativ gleichmäßig, leicht multimodal, Bereich 50-60, mehrere Peaks.
- Interpretation: Fahrzeughöhen variieren stärker als Länge oder Breite.
- curbweight
- Verteilung: Leicht rechtsschief, Hauptteil zwischen 2000-3000, Peak um 2200-2400.
- Interpretation: Die meisten Fahrzeuge sind im unteren bis mittleren Gewichtbereich.
- enginesize
- Verteilung: Stark rechtsschief, dominanter Peak bei kleineren Motorgrößen (~100).
- Interpretation: Kleine Motoren sind häufig, große Motoren selten.
- boreratio
- Verteilung: Gleichmäßig, leicht multimodal, Bereich 3.0-3.5.
- Interpretation: Zeigt verschiedene gängige Motorbohrungsverhältnisse.
- stroke
- Verteilung: Glockenförmig, leicht linksschief, Peak um 3.3.
- Interpretation: Hub konzentriert sich auf einen klaren Bereich.
- compressionratio
- Verteilung: Stark rechtsschief, dominanter Peak 9-10, kleiner zweiter Peak 20-22.
- Interpretation: Die meisten Autos haben typische Benzin-Kompressionsverhältnisse; höhere Werte deuten auf Diesel hin.
- horsepower
- Verteilung: Stark rechtsschief, dominanter Peak bei 60-80 PS.
- Interpretation: Die Mehrheit der Fahrzeuge hat geringere Leistung; sehr starke Fahrzeuge sind selten.
- peakrpm
- Verteilung: Glockenförmig, leicht rechtsschief, Peak um 5000 U/min.
- Interpretation: Maximaldrehzahl liegt bei den meisten Fahrzeugen um 5000 U/min.
- citympg
- Verteilung: Stark rechtsschief, Peak bei 20-25 MPG.
- Interpretation: Die meisten Fahrzeuge haben durchschnittlichen Stadtverbrauch, sparsame Autos sind seltener.
- highwaympg
- Verteilung: Stark rechtsschief, Peak bei 25-30 MPG.
- Interpretation: Autobahnverbrauch ist tendenziell besser als Stadtverbrauch; sehr sparsame Fahrzeuge sind selten.
2.1.4.2 Zusammenfassende Beobachtungen¶
- Rechtsschiefe Verteilungen: Viele Merkmale (z.B. Zylinderzahl, Motorgröße, Leistung, Curbweight, City/Highway MPG) weisen rechtsschiefe Verteilungen auf. Hohe Werte sind selten.
- Kategoriale/Diskrete Werte: "Symboling" und "doornumber" sind diskret oder kategorial.
- Normalverteilungsähnlichkeit: Merkmale wie "wheelbase", "carlength", "carwidth", "peakrpm", "stroke" ähneln einer Normalverteilung.
- Bimodale Verteilungen: "doornumber" und "compressionratio" zeigen zwei Gruppen (z.B. 2- vs. 4-türige Autos, Benzin- vs. Diesel-Motoren).
Fazit:
Die Histogramme liefern einen wertvollen Überblick über die Verteilung der Fahrzeugmerkmale, helfen, Ausreißer zu erkennen und identifizieren typische Wertebereiche sowie gemeinsame Muster im Datensatz.
# Feature engneering:
# Histogram für numerische Werte erstellen und darstellen
numerical_columns = ['symboling','doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight',
'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
'peakrpm', 'citympg', 'highwaympg']
cols = 5
rows = int(np.ceil(len(numerical_columns) / cols))
fig = make_subplots(rows=rows, cols=cols, subplot_titles=numerical_columns)
for i, feature in enumerate(numerical_columns):
row = i // cols + 1
col = i % cols + 1
fig.add_trace(
go.Histogram(
x=df[feature],
nbinsx=25,
name=feature,
marker=dict(
line=dict(
color='black', # Linienfarbe
width=1 # Liniendicke
)
)
),
row=row, col=col
)
fig.update_layout(
height=300 * rows,
width=1400,
title_text="Histogramme der numerischen Features",
showlegend=False
)
fig.show()
2.1.5 Histogram von Autopreisen: hier von besonderem Interesse¶
Es geht um folgende Rahmenkennzahlen der Autopreise
Min.: 5118€¶
Max.: 45400€¶
mean: 13276€¶
midian: 10295€¶
Es ist zu erkennen, dass die meisten Autos im Preisbereich zwischen 6.000 und 20.000 $ liegen.
Das bedeutet, dass die Daten überwiegend Fahrzeuge aus dem niedrigen und mittleren Preissegment abbilden.
Daraus lässt sich schließen, dass die Modellierung und die Preisvorhersage in erster Linie auf diese Segmente ausgelegt ist.
Für das hochpreisige Segment sind dagegen deutlich weniger Datenpunkte vorhanden, was die Prognosegenauigkeit in diesem Bereich einschränken kann.
Auf Grundlage dieser Erkenntnisse muss entschieden werden, ob die Daten für das hochpreisige Segment überhaupt sinnvoll für die Modellierung und Preisvorhersage genutzt werden können.
Da in diesem Bereich nur sehr wenige Datenpunkte vorliegen, besteht die Gefahr, dass das Modell hier keine zuverlässigen Zusammenhänge lernen kann.
Dies kann die Qualität und Aussagekraft des Modells im oberen Preissegment deutlich verschlechtern und zu ungenauen oder verzerrten Vorhersagen führen.
Eine mögliche Konsequenz wäre, das Modell bewusst auf das niedrige und mittlere Preissegment zu beschränken oder für das hochpreisige Segment zusätzliche Datenquellen zu erschließen.
# Analyse von Preis: Histogram für Preise
print("-- Überblick über statistische Werte von Preis --")
print("min.:", df['price'].min())
print("max.:",df['price'].max())
print("mean:",df['price'].mean())
print("median:",df['price'].median())
fig = px.histogram(df, x='price', nbins=40, marginal="rug", title='Distribution of Price', opacity=0.8)
fig.update_layout(
xaxis_title='Price',
yaxis_title='Count',
height=600 # Höhe des Plots
)
fig.update_xaxes(
tickmode='linear',
tick0=0,
dtick=2000
)
fig.show()
-- Überblick über statistische Werte von Preis -- min.: 5118.0 max.: 45400.0 mean: 13276.710570731706 median: 10295.0
2.1.6 Bar-Darstellung der kategorischen Daten¶
print(df['fuelsystem'].unique())
print(df['enginetype'].unique())
print(df['carbody'].unique())
['mpfi' '2bbl' 'mfi' '1bbl' 'spfi' '4bbl' 'idi' 'spdi'] ['dohc' 'ohcv' 'ohc' 'l' 'rotor' 'ohcf' 'dohcv'] ['convertible' 'hatchback' 'sedan' 'wagon' 'hardtop']
import plotly.subplots as sp
categorical_columns = ['fueltype', 'aspiration', 'carbody', 'drivewheel',
'enginelocation', 'enginetype', 'fuelsystem']
df_cat = df.copy()
for c in categorical_columns:
df_cat[c] = (df_cat[c].astype('string').str.strip().fillna('dl'))
fig = sp.make_subplots(rows=3, cols=3,
subplot_titles=[col.capitalize() for col in categorical_columns])
row, col = 1, 1
for column in categorical_columns:
vc = (df_cat[column]
.value_counts(dropna=False) # alle mitnehmen
.sort_index()) # fixe Reihenfolge (oder sort_values())
cats = list(map(str, vc.index)) # Kategorien als Strings
fig.add_trace(
go.Bar(x=cats, y=vc.values, name=column),
row=row, col=col
)
fig.update_xaxes(
type='category',
title_text=column.capitalize(),
categoryorder="array",
categoryarray=cats,
tickangle=45,
row=row, col=col
)
fig.update_yaxes(title_text="Count", row=row, col=col)
col += 1
if col > 3:
col = 1
row += 1
fig.update_layout(height=900, width=1300, showlegend=False,
title_text="Categorical Features Count Plots",
bargap=0.15)
fig.show()
ich habe kategorischen Variablen zunächst exploriert (Häufigkeiten, dominante Kategorien, seltene Kategorien) und anschließend überlegt, ob und wie man diese in numerische Faktoren übersetzen könnte.
Die Logik:
Dominante Kategorie = Basisfaktor 1.0
Weniger dominante Kategorie = etwas >1
Seltene oder fachlich teurere Kategorie = größerer Faktor
Teilweise fachlich begründet (z. B. Diesel teurer als Gas).
Das ist ein Mix aus fachlichem Wissen und heuristischer Gewichtung – allerdings ist das nicht die Standardmethode im Machine Learning (da nimmt man z. B. One-Hot-Encoding, Ordinal-Encoding oder Target-Encoding). Trotzdem finde ich sinnvoll, diesen Ansatz als Überlegung sauber zu dokumentieren.
Kategorien und Überlegung zur Faktorisierung
Bei der Analyse der kategorialen Merkmale fällt auf, dass manche Kategorien sehr dominant sind (z. B. Gas bei Fueltype, OHC bei Engine Type, Front bei Engine Location), während andere nur selten vorkommen. Für Machine Learning ist es jedoch erforderlich, diese Kategorien in numerische Werte umzuwandeln.
Eine Überlegung ist, Faktoren zu vergeben:
Dominante Kategorie → Basiswert 1.0
Weniger dominante, aber relevante Kategorie → Wert leicht über 1.0
Sehr seltene Kategorie → noch etwas größerer Faktor
Zusätzlich: fachliche Begründung (z. B. Diesel i. d. R. teurer als Benzin).
Tabellarische Zusammenfassung:
| Feature | Kategorien (mit Häufigkeit) | Faktor-Überlegung / Begründung | Beispiel-Faktoren |
|---|---|---|---|
| Fueltype | Gas (185), Diesel (20) | Diesel oft teurer als Benzin | Gas = 1.0, Diesel = 1.2 |
| Aspiration | std (168), turbo (37) | Turbo = leistungssteigernd, höherer Preis | std = 1.0, turbo = 1.3 |
| Carbody | convertible (6), hardtop (8), hatchback (70), sedan (96), wagon (25) | Sedan als Basis, convertible/hardtop teurer, hatchback günstiger | sedan = 1.0, hatchback = 1.1, wagon = 1.2, convertible = 2.0, hardtop = 2.3 |
| Drivewheel | fwd (120), rwd (76), 4wd (9) | fwd = Standard, rwd = sportlicher/teurer, 4wd selten/luxus | fwd = 1.0, rwd = 1.2, 4wd = 2.0 |
| Enginelocation | front (202), rear (3) | Rear = selten, meist Sportwagen → teurer | front = 1.0, rear = 1.2 |
| Enginetype | ohc (148), dohc (12), dohcv (1), l (12), ohcf (15), ohcv (13), rotor (4) | ohc = Standard, andere Bauarten seltener/teurer | ohc = 1.1, l = 1.0, dohc = 1.3, dohcv = 1.4, ohcf = 1.1, ohcv = 1.2, rotor = 1.5 |
| Fuelsystem | mpfi (94), 2bbl (66), idi (20), 1bbl (11), spdi (9), 4bbl (3), mfi (1), spfi (1) | mpfi als Basis, seltenere Systeme teurer oder spezieller | mpfi = 1.0, 2bbl = 1.1, idi = 1.3, 1bbl = 1.5, spdi = 1.7, 4bbl = 1.4, mfi = 1.6, spfi = 1.8 |
Bewertung des Ansatzes
Dieser Ansatz liefert eine erste Annäherung an eine numerische Abbildung kategorialer Variablen.
Allerdings ist die Faktorisierung subjektiv und basiert auf einer Mischung aus fachlichem Wissen und Häufigkeit der Kategorien.
In der Praxis würden Datenanalysten eher Verfahren wie One-Hot-Encoding oder Target-Encoding verwenden, da diese Methoden objektiver und reproduzierbarer sind.
Meine Überlegung stellt einen spezifischen Ansatz dar und dient in erster Linie dazu, ein datenbasiertes, besseres Verständnis der Merkmale zu gewinnen. Es handelt sich dabei nicht um ein endgültiges Vorgehen für die Modellierung, sondern um eine gedankliche Stütze, wie man als Datenanalyst die Daten interpretieren und deren Einfluss einschätzen kann.
Für die eigentliche Modellierung werde ich weiterhin die etablierten Standardverfahren einsetzen.
2.1.7 Überblick über die Autohersteller:¶
Hier werden die Top-20 Autohersteller in der Daten dargestellt. z.B. Das Modell Peugeot 504 kommt 6-mal im Datensatz vor.
n = 20 # Number of top car models to plot
top_car_models = df['CarName'].value_counts().head(n)
plt.figure(figsize=(10, 6))
sns.barplot(x=top_car_models.values, y=top_car_models.index)
plt.title(f'Top {n} Car Models by Frequency')
plt.xlabel('Frequency')
plt.ylabel('Car Model')
plt.tight_layout()
plt.show()
2.1.8 Berechnung von Durchschnittpreis für Top-20 Autohersteller¶
Hier werden die teuersten Top-20 Autohersteller in der Daten dargestellt.
# Calculate average price for each car model
avg_prices_by_car = df.groupby('CarName')['price'].mean().sort_values(ascending=False)
# Plot top N car models by average price
n = 20 # Number of top car models to plot
top_car_models = avg_prices_by_car.head(n)
plt.figure(figsize=(10, 6))
sns.barplot(x=top_car_models.values, y=top_car_models.index)
plt.title(f'Top {n} Car Models by Average Price')
plt.xlabel('Average Price')
plt.ylabel('Car Model')
plt.tight_layout()
plt.show()
2.1.9 Durchschnittspreise der Hersteller im unteren Preissegment (günstigste 20 Autohersteller)¶
Hier werden die günstisten Top-20 Autohersteller in der Daten dargestellt.
# Calculate average price for each car model
avg_prices_by_car = df.groupby('CarName')['price'].mean().sort_values(ascending=True)
# Plot top N car models by average price
n = 20
top_car_models = avg_prices_by_car.head(n)
plt.figure(figsize=(10, 6))
sns.barplot(x=top_car_models.values, y=top_car_models.index)
plt.title(f' Cheapest {n} Car Models by Average Price')
plt.xlabel('Average Price')
plt.ylabel('Car Model')
plt.tight_layout()
plt.show()
2.1.10 Kategorische Merkmale vs. Preis: Boxplot¶
Der Boxplot ist ein zentrales Werkzeug in der Datenanalyse. Er visualisiert die Verteilung von Daten und macht wichtige Eigenschaften wie Lage, Streuung und mögliche Ausreißer auf einen Blick erkennbar.¶
1. Interquartile Range (IQR)
Der Interquartilbereich (IQR) beschreibt die Spannweite der mittleren 50 % der Daten.
Q1 (25. Perzentil): Wert, unter dem 25 % der Daten liegen.
Q2 (Median, 50. Perzentil): Wert, unter dem 50 % der Daten liegen.
Q3 (75. Perzentil): Wert, unter dem 75 % der Daten liegen.
Formel für IQR:
IQR=Q3−Q1
Der IQR gibt also die Spannweite des „mittleren Blocks“ der Daten an – er ignoriert die extremen Ausreißer.
Beispiel: Wenn Q1 = 10 und Q3 = 20 → IQR = 20 − 10 = 10.
2. Whiskers (typisch 1.5×IQR)
Die Whiskers in einem Boxplot zeigen die Daten „außerhalb“ des mittleren Bereichs, aber ohne Ausreißer.
Standardregel:
Unteres Whisker-Limit: Q1−1.5×IQR
Oberes Whisker-Limit: Q3+1.5×IQR
Alles unterhalb von Q1 − 1.5×IQR oder oberhalb von Q3 + 1.5×IQR wird als Ausreißer markiert (oft Punkte außerhalb des Whiskers).
Beispiel:
Q1 = 10, Q3 = 20 → IQR = 10
Unteres Limit: 10 − 1.5×10 = −5
Oberes Limit: 20 + 1.5×10 = 35
Wenn deine Daten von 0 bis 40 reichen:
Whisker zeigen nur 0 bis 35
Werte >35 (z. B. 37, 40) sind Ausreißer.
3. Visualisierung im Boxplot
┌─────────────┐ ← Q3 (75%)
│ │
│ Box │ ← mittlere 50% (Q1–Q3)
│ │
└─────────────┘ ← Q1 (25%)
Whisker: │
│
└── Punkte = Ausreißer (>1.5×IQR)
Zusammenhang zwischen Whisker und 3-facher Standardabweichung¶
Da das Thema Ausreißererkennung hier behandelt wird, ist es sinnvoll einen Vergeleich der zwei Methoden zur Ausreißererkennung zu machen
1️⃣ Whisker (Boxplot) – IQR-Methode¶
- IQR = Interquartilsabstand = Q3 − Q1
- Unteres Whisker-Limit:
Q1 - 1.5 × IQR - Oberes Whisker-Limit:
Q3 + 1.5 × IQR
Eigenschaften:
- Robust gegenüber Ausreißern, da nur die mittleren 50 % der Daten betrachtet werden.
- Funktioniert auch bei schiefen oder nicht-normalverteilten Daten gut.
2️⃣ 3-fache Standardabweichung – Z-Score-Methode¶
- Mittelwert: μ
- Standardabweichung: σ
- Unteres Limit:
μ - 3σ - Oberes Limit:
μ + 3σ
Eigenschaften:
- Annahme: Daten sind normalverteilt.
- Laut 68–95–99.7-Regel liegen ~99.7 % der Werte innerhalb von ±3σ.
- Ausreißer sind Werte >3σ vom Mittelwert entfernt.
3️⃣ Vergleich / Zusammenhang¶
| Aspekt | IQR (Whisker) | 3σ-Methode |
|---|---|---|
| Basis | Quartile (Q1, Q3) | Mittelwert & Standardabweichung |
| Robustheit | Sehr robust gegenüber Ausreißern | Weniger robust, Ausreißer beeinflussen σ |
| Verteilung | Keine Annahmen | Annahme: Normalverteilung |
| Typische Erkennung | Werte >1.5×IQR vom Quartil entfernt | Werte >3σ vom Mittelwert entfernt |
| Sensitivität | Moderate Ausreißer | Extreme Ausreißer bei Normalverteilung |
Fazit:
- Beide Methoden erkennen Ausreißer, aber Whisker basiert auf der mittleren Hälfte der Daten und ist robuster bei Schiefe oder Extremwerten.
- Die 3σ-Regel eignet sich besonders für normalverteilte Daten.
- Bei symmetrischer, ungefähr normalverteilter Datenmenge sind die Grenzen oft ähnlich, aber nicht identisch.
4. Intepretation der Ergebnisse vom Boxplot:
4.1 fueltype vs. Price
Gas ist der häufigste Treibstofftyp. Die Preise liegen meist zwischen 6.000 – 20.000 $,
mit Ausreißern bis über 30.000 $.
Diesel-Autos scheinen im Median etwas teurer als Gas-Autos, aber die Stichprobe ist kleiner.
👉 Interpretation: Dieselkraftstoff korreliert mit tendenziell höheren Preisen.
4.2 aspiration vs. Price
Standard-Aspiration (std: Luftzufuhr ohne Turbo oder Kompressor) hat eine breite Preisverteilung, Median im mittleren Bereich.
Turbo-Autos sind im Median teurer, aber auch mit hoher Varianz.
👉 Interpretation: Turbo-Autos könnten ein Premium-Segment darstellen.
4.3 carbody vs. Price
Sedan und Hatchback dominieren und liegen im mittleren bis unteren Preisbereich.
Convertible (Cabrio) und Hardtop zeigen höhere Medianpreise und größere Varianz.
👉 Interpretation: Die Karosserieform hat deutlichen Einfluss auf den Preis, Premium-Karosserien (Cabrio, Hardtop) → teurer.
4.4. drivewheel vs. Price
RWD (Heckantrieb) → tendenziell teurere Autos, größerer Median.
FWD (Frontantrieb) → günstigere Autos, am häufigsten vertreten.
4WD (Allrad) → wenige Datenpunkte, Median im mittleren Bereich.
👉 Interpretation: Heckantrieb wird eher bei teuren Autos verwendet (Sportwagen/Luxusautos).
4.5 enginelocation vs. Price
Front → fast alle Autos, Preise meist unter 20.000 $.
Rear (Heckmotor) → sehr wenige Autos, aber deutlich höherer Preisbereich.
👉 Interpretation: Heckmotor ist selten, typisch für Sportwagen, deutlich teurer.
4.6 enginetype vs. Price
ohc (Overhead Camshaft) → am häufigsten, Preise im unteren bis mittleren Bereich.
dohcv und rotor → selten, aber sehr hochpreisig.
ohcf und ohcv → mittleres Segment.
👉 Interpretation: Bestimmte Motortypen (z. B. dohc, rotor) sind mit Premiumautos assoziiert.
4.7 fuelsystem vs. Price
Sehr viele Systeme im Vergleich, z. B. mpfi (häufig) → mittlerer Preisbereich.
idi und spdi → sehr kleine Gruppen, mit hohen Ausreißern.
👉 Interpretation: Das Kraftstoffsystem spielt eine Rolle, aber die meisten Systeme liegen im günstigen Bereich; nur wenige Systeme (idi, spdi) korrelieren mit hohen Preisen.
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from math import ceil
# Categorical Feature vs. Price
n_cols = 3
n_rows = ceil(len(categorical_columns) / n_cols)
fig = make_subplots(rows=n_rows, cols=n_cols, subplot_titles=categorical_columns)
for idx, feature in enumerate(categorical_columns):
row = idx // n_cols + 1
col = idx % n_cols + 1
# Einzelner Boxplot mit plotly.express erzeugen
box_fig = px.box(df, x=feature, y='price', points='outliers', color_discrete_sequence=["blue"])
# Traces in die Subplots einfügen
for trace in box_fig.data:
fig.add_trace(trace, row=row, col=col)
fig.update_layout(height=450 * n_rows, width=500 * n_cols, showlegend=False)
fig.show()
2.1.11 Numerische Werte mit Bar-Darstellung¶
Normalerweise werden numerische Werte mit einem Histogramm dargestellt, da dies die Verteilung der Werte zeigt. In diesem Fall werden sie jedoch mit einem Balkendiagramm (Barplot) abgebildet, um gezielt die Anzahl der Messwerte in bestimmten Kategorien oder Bereichen hervorzuheben und mehr Details sichtbar zu machen.
# jeder Werte zeigen, ähnlich wie histogramm (Werte aber zusammenfasst)
numerical_columns = ['symboling','doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight',
'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
'peakrpm', 'citympg', 'highwaympg']
single_color = 'blue' # eine Farbe für alle Plots
fig = make_subplots(
rows=8, cols=2,
subplot_titles=[col.capitalize() for col in numerical_columns],
horizontal_spacing=0.05, # engerer horizontaler Abstand
vertical_spacing=0.08
)
row, col = 1, 1
for col_name in numerical_columns:
counts = df[col_name].value_counts().reset_index()
counts.columns = [col_name, 'Count']
fig.add_trace(
go.Bar(
x=counts[col_name],
y=counts['Count'],
text=counts['Count'],
textposition='outside',
marker_color=single_color
),
row=row, col=col
)
col += 1
if col > 2: # nur 2 Spalten
col = 1
row += 1
fig.update_layout(
height=2200, width=1600, # Höhe und Breite anpassen
title_text="Count Plots of Numerical Columns",
showlegend=False
)
fig.show()
2.2 Explorative Datenanalyse (EDA)¶
Explorative Datenanalyse (EDA) ist ein methodischer Ansatz zur ersten Untersuchung von Datensätzen ohne vorgegebene Hypothesen. Ziel ist es, durch statistische und visuelle Methoden Muster, Zusammenhänge, Ausreißer und Datenstrukturen zu erkennen. Dabei hilft EDA, ein besseres Verständnis der Daten zu erlangen, Hypothesen zu generieren und die Datenqualität zu überprüfen.
EDA verwendet häufig Streudiagramme und Korrelationsanalysen, um Zusammenhänge zwischen Variablen zu untersuchen, während die deskriptive Statistik eher einfache Zusammenfassungen und Standardvisualisierungen liefert.
2.2.1 Streudiagramm: Preis (y-Achse) vs. numerische Spalte (x-Achse)¶
Ein Streudiagramm ist für die explorative Datenanalyse (EDA) sehr nützlich, weil es:
- Zusammenhänge zwischen zwei numerischen Variablen sichtbar macht
- Zeigt, ob eine positive, negative oder keine Korrelation vorliegt
- Hilfreich ist, um Muster, Trends oder Ausreißer zu erkennen
- Visuell sofort Einblicke in die Datenstruktur und mögliche Abhängigkeiten gibt
Kurz:
Ein Streudiagramm unterstützt in der EDA dabei, Beziehungen in den Daten zu verstehen und auf Basis dieser Erkenntnisse weitere Analyseschritte zu planen.
Interpretation der Scatterplots (Features vs. Preis)¶
1. Abmessungen¶
- carlength vs price:
Längere Autos → tendenziell teurer. Positiver Zusammenhang, aber mit Streuung. - carwidth vs price:
Breitere Autos → deutlich teurer. Klarer linearer Trend. - carheight vs price:
Kein klarer Trend. Manche hohe Autos sind teuer, andere nicht → schwacher Zusammenhang. - wheelbase vs price:
Längere Radstände → etwas teurer, aber viele Ausreißer. Mittlerer Zusammenhang.
2. Gewicht & Motorgröße¶
- curbweight vs price:
Sehr starker positiver Zusammenhang → schwerere Autos sind teurer. - enginesize vs price:
Sehr klarer Trend → größere Motoren bedeuten höhere Preise. Einer der besten Prädiktoren. - cylindernumber vs price:
Mehr Zylinder → höhere Preise. Besonders von 3–4 (günstig) zu 6–8 (teuer). Wenige Autos mit 10–12 Zylindern, aber diese sind extrem teuer. Sehr guter Indikator. - boreratio vs price:
Leichter positiver Trend. Große Streuung, daher nur mäßig stark.
3. Motorcharakteristik¶
- stroke vs price:
Kaum Zusammenhang, stark verstreute Werte. - compressionratio vs price:
Kein klarer linearer Trend. Einzelne Autos mit hoher Kompression → teurer, aber unregelmäßig. - horsepower vs price:
Sehr deutlicher positiver Zusammenhang. Leistungsstärkere Autos kosten klar mehr. Einer der stärksten Prädiktoren. - peakrpm vs price:
Kein klarer Trend, Daten stark verstreut. Schwacher Prädiktor.
4. Verbrauchswerte¶
- citympg vs price:
Negativer Zusammenhang: sparsame Autos (hoher mpg) sind günstiger. - highwaympg vs price:
Ähnlich wie citympg: höhere Effizienz → niedrigere Preise. Moderater negativer Zusammenhang.
5. Karosserie & Türen¶
- doornumber vs price:
Kein klarer Unterschied zwischen 2- und 4-Türern. Schwacher Einfluss auf den Preis.
✅ Gesamtbewertung¶
Sehr starke Prädiktoren (hohe Aussagekraft):
enginesize,curbweight,horsepower,cylindernumber,carwidth,carlength.Mittlere Prädiktoren:
wheelbase,boreratio,citympg,highwaympg.Schwache/kaum relevante Prädiktoren:
carheight,stroke,compressionratio,peakrpm,doornumber.
# Feuture engineering for numerical_columns: Scatter plot
numerical_columns = ['doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight',
'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
'peakrpm', 'citympg', 'highwaympg']
target = 'price'
# Annahme: df ist dein DataFrame und features_for_scatter deine Liste der Features
# Beispielhafte Berechnung der Achsenbereiche, wie zuvor bestimmt (hier musst du ggf. deine realen Daten nutzen)
features_for_scatter = [f for f in numerical_columns if f != target]
print(features_for_scatter)
feature_ranges = {
feature: (df[feature].min(), df[feature].max()) for feature in features_for_scatter
}
# Subplot-Layout mit 3 Spalten und entsprechender Zeilenanzahl
num_features = len(features_for_scatter)
num_cols = 3
num_rows = (num_features + num_cols - 1) // num_cols
fig = make_subplots(
rows=num_rows, cols=num_cols,
subplot_titles=[f"{feat} vs {target}" for feat in features_for_scatter],
vertical_spacing=0.08 # Weniger Abstand zwischen den Reihen
)
#fig = make_subplots(rows=num_rows, cols=num_cols,subplot_titles=[f"{feat} vs {target}" for feat in features_for_scatter])
for i, feature in enumerate(features_for_scatter):
row = i // num_cols + 1
col = i % num_cols + 1
# 5% Padding links und rechts für x-Achse
x_min, x_max = feature_ranges[feature]
x_range_pad = (x_max - x_min) * 0.05
x_axis_range = [x_min - x_range_pad, x_max + x_range_pad]
scatter = go.Scatter(x=df[feature], y=df[target], mode='markers', name=feature)
fig.add_trace(scatter, row=row, col=col)
# Setze die x-Achse des Subplots mit Range und angemessener Tick-Anzahl
fig.update_xaxes(range=x_axis_range, row=row, col=col, nticks=5)
# Label für jede Achse hinzufügen (optional)
fig.update_xaxes(title_text=feature, row=row, col=col)
fig.update_yaxes(title_text=target, row=row, col=col)
# Layout anpassen: Grösse, Abstand, Titel
fig.update_layout(height=300*num_rows, width=1300, # statt 1000 jetzt 1200 für +20%
title_text="Scatterplots Features vs. price (3 Spalten Layout)",
showlegend=False,
margin=dict(l=40, r=40, t=80, b=40))
fig.show()
['doornumber', 'cylindernumber', 'wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight', 'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower', 'peakrpm', 'citympg', 'highwaympg']
2.2.2 Korrelationskarte (Heatmap)¶
1. Correlation Map of numerical_columns¶
Eine Korrelationskarte (Heatmap) zeigt die Zusammenhänge zwischen allen numerischen Spalten in einer Matrixform.
- Jede Zelle enthält den Korrelationskoeffizienten (z. B. Pearson), der angibt, wie stark zwei Variablen linear miteinander zusammenhängen (Werte zwischen -1 und +1).
- Positive Werte → beide Variablen steigen oder fallen gemeinsam.
- Negative Werte → wenn eine steigt, sinkt die andere.
- Werte nahe 0 → kein linearer Zusammenhang.
2. Warum ist das gut?¶
- Erkennt starke Prädiktoren für die Zielvariable (z. B.
price). - Hilft, Multikollinearität zu identifizieren (z. B.
curbweightundenginesizekönnten stark korrelieren). - Unterstützt bei der Feature Selection, um redundante Variablen zu vermeiden.
- Liefert eine schnelle visuelle Übersicht, welche numerischen Features wichtig sein könnten.
# correlation map for numerical_columns:
numerical_columns1 = ['doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight',
'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
'peakrpm', 'citympg', 'highwaympg','price']
correlation_matrix = df[numerical_columns1].corr().round(2)
# Heatmap mit Plotly Express
fig = px.imshow(
correlation_matrix,
text_auto=True, # Werte direkt in die Zellen schreiben
color_continuous_scale='RdBu_r', # entspricht 'coolwarm'
title="Correlation Heatmap"
)
fig.update_layout(width=1200, height=1200)
fig.show()
3. Korrelation der Features mit dem Fahrzeugpreis¶
| Feature (Merkmal) | Korrelation | Richtung |
|---|---|---|
| enginesize (Motorgröße) | 0.87 | positiv |
| curbweight (Leergewicht) | 0.84 | positiv |
| horsepower (PS) | 0.81 | positiv |
| carwidth (Fahrzeugbreite) | 0.76 | positiv |
| cylindernumber (Zylinderanzahl) | 0.72 | positiv |
| highwaympg (Verbrauch Autobahn) | -0.71 | negativ |
| citympg (Verbrauch Stadt) | -0.70 | negativ |
| carlength (Fahrzeuglänge) | 0.68 | positiv |
| wheelbase (Radstand) | 0.57 | positiv |
| boreratio (Bohrungsverhältnis) | 0.55 | positiv |
| carheight (Fahrzeughöhe) | 0.11 | positiv |
| peakrpm (max. Drehzahl) | -0.09 | negativ |
| stroke (Hub) | 0.08 | positiv |
| compressionratio (Verdichtungsverhältnis) | 0.07 | positiv |
| doornumber (Anzahl der Türen) | 0.02 | positiv |
4. Analyse der Korrelationen mit dem Fahrzeugpreis¶
Die Analyse der Merkmale zeigt, dass bestimmte Fahrzeugparameter stark mit dem Fahrzeugpreis korrelieren:
Starke positive Korrelation:
- Motorgröße (enginesize)
- Leergewicht (curbweight)
- PS (horsepower)
Fahrzeuge mit größeren Motoren, höherem Gewicht und mehr Leistung tendieren dazu, teurer zu sein.
Weitere positive Einflüsse:
- Fahrzeugbreite (carwidth)
- Zylinderanzahl (cylindernumber)
- Fahrzeuglänge (carlength)
- Radstand (wheelbase)
- Bohrungsverhältnis (boreratio)
Diese Merkmale tragen ebenfalls positiv zum Preis bei, wenn auch moderater.
Negative Korrelation:
- Verbrauch auf der Autobahn (highwaympg)
- Verbrauch in der Stadt (citympg)
Autos mit besserer Kraftstoffeffizienz sind tendenziell günstiger.
Geringe oder kaum relevante Korrelation:
- Fahrzeughöhe (carheight)
- Maximale Drehzahl (peakrpm)
- Hub (stroke)
- Verdichtungsverhältnis (compressionratio)
- Anzahl der Türen (doornumber)
Diese Merkmale haben nur einen schwachen Einfluss auf den Preis.
Fazit:
Motorgröße, Leergewicht und PS sind die stärksten positiven Indikatoren für den Fahrzeugpreis. Fahrzeugbreite und Zylinderanzahl sind ebenfalls wichtig. Die Kraftstoffeffizienz (highwaympg, citympg) wirkt stark negativ auf den Preis. Ähnlich wie im Streudiagramm spiegeln die Korrelationswerte die gleichen Zusammenhänge wider.
Mit dieser Erkenntnis lässt sich folgende Frage des Unternehmens gut beantworten:
Das Unternehmen möchte wissen:
- Welche Variablen sind signifikant für die Vorhersage des Autopreises?
- Wie gut erklären diese Variablen den Preis eines Autos?
3. Data Preparation (Datenaufbereitung)¶
Für Machine-Learning ist sehr wichtig, eine gute aufbereitete Daten zu haben, um ein allgemein aussagkräftiges Modell zu tranineren.
3.1. Datenbereinigung¶
- car_ID: eine laufende Nummer, keine Bedeutung für Datenanalyse und maschinelles Lernen. Löschen
- citympg und hightwaympg: die Verteilung wird noch mal genau angeschaut. Die Aussereisser werden bereinigt. siehe Punkt 3.5
3.2. Umwandlung von kategorischen Daten in numerischen Daten¶
Anzahl von Türen und Zylindern sind als kategorische Daten dargestellt. sie werden in Zahlen umgewandelt(schon erledigt in der Deskriptiven Analyse)
3.3. Zerlegung von kategorischen Daten in zwei Spalten: brand und model¶
In der Spalte 'CarName' sind Information über Marke und Modelle zusammengefasst. Sie sollen in zwei Spalten dargestellt werden
3.4. Anwendung von log(y),sqrt(y), x^2 usw. auf bestimmten Spalten: x: Preis, y: Werte in der Spalten¶
was bedeutet das z.B. log() Transformation: wenn y Werte (Enginesize) eine ideale Funktion von y= e^x, x(Preis), dannn log(y) wird dann linear zu x(Preis)
Logarithmen helfen, schiefe (skewed) Verteilungen oder extrem große Wertebereiche, besser für Modelle zu optimieren.
Das geiche gilt für andere funktionale Beziehung wie sqrt() usw.
die zwei Spalten 'enginesize' und 'compressionratio' werden nach der Datenanalyse auf log() angewendet.
3.5. Analyse von Ausreißern in citympg und highwaympg¶
3.5.1 Statistische Betrachtung:¶
- Was ist die Standardabweichung (σ oder std)?
Die Standardabweichung misst, wie stark die einzelnen Werte einer Datenmenge im Durchschnitt von deren Mittelwert (Durchschnitt) abweichen.
Kleine Standardabweichung → die Werte liegen eng um den Mittelwert herum (geringe Streuung).
Große Standardabweichung → die Werte sind weit vom Mittelwert verteilt (hohe Streuung).
Formel (für eine Stichprobe):
wobei xi = einzelner Wert, xˉ = Mittelwert, n = Anzahl der Werte.
- Was bedeutet „3-fache Standardabweichung“? Wenn die Daten annähernd normalverteilt (Glockenkurve) sind, gilt die sogenannte 68–95–99,7-Regel:
±1 σ → ca. 68% aller Werte liegen innerhalb einer Standardabweichung vom Mittelwert.
±2 σ → ca. 95% aller Werte liegen innerhalb von zwei Standardabweichungen.
±3 σ → ca. 99,7% aller Werte liegen innerhalb von drei Standardabweichungen.
- Das heißt: die Werte außer ±3σ werden beseitigt. Werte, die mehr als 3 Standardabweichungen vom Mittelwert entfernt sind, treten sehr selten auf (~0,3% Wahrscheinlichkeit). In der Statistik werden diese oft als Ausreißer betrachtet.
#sns.pairplot(df)
3.5.2 Analyse von citympg und highwaympg:¶
# # Analyse von citympg und highwaympg:
print("-- statistische Werte von highwaympg --")
print("min.:", df['highwaympg'].min())
print("max.:",df['highwaympg'].max())
print("mean:",df['highwaympg'].mean())
print("median:",df['highwaympg'].median())
print("std:",df['highwaympg'].std())
print("6*std:",3*df['highwaympg'].std())
fig = px.histogram(df, x='highwaympg', nbins=30, marginal="rug", title='Distribution of highwaympg', opacity=0.8)
fig.update_layout(
xaxis_title='highwaympg',
yaxis_title='Count',
height=600 # Höhe des Plots
)
fig.update_xaxes(
tickmode='linear',
tick0=0,
dtick=1 # Tick-Beschriftungen alle 2 Einheiten
)
fig.show()
print("-- statistische Werte von citympg --")
print("min.:", df['citympg'].min())
print("max.:",df['citympg'].max())
print("mean:",df['citympg'].mean())
print("median:",df['citympg'].median())
print("std:",df['citympg'].std())
print("6*std:",3*df['citympg'].std())
fig = px.histogram(df, x='citympg', nbins=30, marginal="rug", title='Distribution of citympg', opacity=0.8)
fig.update_layout(
xaxis_title='citympg',
yaxis_title='Count',
height=600 # Höhe des Plots
)
fig.update_xaxes(
tickmode='linear',
tick0=0,
dtick=1 # Tick-Beschriftungen alle 2 Einheiten
)
fig.show()
-- statistische Werte von highwaympg -- min.: 16 max.: 54 mean: 30.75121951219512 median: 30.0 std: 6.886443130941824 6*std: 20.659329392825473
-- statistische Werte von citympg -- min.: 13 max.: 49 mean: 25.21951219512195 median: 24.0 std: 6.542141653001622 6*std: 19.626424959004865
5.5.3 Löschbedingung von Ausreißern: Spalte:'highwaympg'¶
mean: 30.75121951219512¶
std: 6.886443130941824¶
limit_right: mean+3std=30.75+36.88(20.64)= ca. 52; Werte > 52 werden gelöscht: nur zwei Werte 53, 54 betroffen¶
limit_left: mean-+3std= 30.75-36.88(20.64)= ca. 10; Werte < 10 werden gelöscht: keine Werte betroffen¶
# werden später beim Modelltraining benutzt
limit_highwaympg_r=52
limit_highwaympg_l=10
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 205 entries, 0 to 204 Data columns (total 26 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 car_ID 205 non-null int64 1 symboling 205 non-null int64 2 CarName 205 non-null object 3 fueltype 205 non-null object 4 aspiration 205 non-null object 5 doornumber 205 non-null int64 6 carbody 205 non-null object 7 drivewheel 205 non-null object 8 enginelocation 205 non-null object 9 wheelbase 205 non-null float64 10 carlength 205 non-null float64 11 carwidth 205 non-null float64 12 carheight 205 non-null float64 13 curbweight 205 non-null int64 14 enginetype 205 non-null object 15 cylindernumber 205 non-null int64 16 enginesize 205 non-null int64 17 fuelsystem 205 non-null object 18 boreratio 205 non-null float64 19 stroke 205 non-null float64 20 compressionratio 205 non-null float64 21 horsepower 205 non-null int64 22 peakrpm 205 non-null int64 23 citympg 205 non-null int64 24 highwaympg 205 non-null int64 25 price 205 non-null float64 dtypes: float64(8), int64(10), object(8) memory usage: 41.8+ KB
5.5.4 Zerlegung von CarName: brand and model¶
# separate brand and model
split_cols = df['CarName'].str.split(' ', n=1, expand=True)
# Spalten zuordnen
df['brand'] = split_cols[0] # erstes Wort
df['model'] = split_cols[1]
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 205 entries, 0 to 204 Data columns (total 28 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 car_ID 205 non-null int64 1 symboling 205 non-null int64 2 CarName 205 non-null object 3 fueltype 205 non-null object 4 aspiration 205 non-null object 5 doornumber 205 non-null int64 6 carbody 205 non-null object 7 drivewheel 205 non-null object 8 enginelocation 205 non-null object 9 wheelbase 205 non-null float64 10 carlength 205 non-null float64 11 carwidth 205 non-null float64 12 carheight 205 non-null float64 13 curbweight 205 non-null int64 14 enginetype 205 non-null object 15 cylindernumber 205 non-null int64 16 enginesize 205 non-null int64 17 fuelsystem 205 non-null object 18 boreratio 205 non-null float64 19 stroke 205 non-null float64 20 compressionratio 205 non-null float64 21 horsepower 205 non-null int64 22 peakrpm 205 non-null int64 23 citympg 205 non-null int64 24 highwaympg 205 non-null int64 25 price 205 non-null float64 26 brand 205 non-null object 27 model 203 non-null object dtypes: float64(8), int64(10), object(10) memory usage: 45.0+ KB
5.5.5 Datenbereinigung: zwei None festgestellt, NONE durch 'dl' (Default Label) ersetzen¶
nan_rows = df[df.isna().any(axis=1)].index
print(nan_rows)
Index([138, 141], dtype='int64')
df = df.fillna("dl")
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 205 entries, 0 to 204 Data columns (total 28 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 car_ID 205 non-null int64 1 symboling 205 non-null int64 2 CarName 205 non-null object 3 fueltype 205 non-null object 4 aspiration 205 non-null object 5 doornumber 205 non-null int64 6 carbody 205 non-null object 7 drivewheel 205 non-null object 8 enginelocation 205 non-null object 9 wheelbase 205 non-null float64 10 carlength 205 non-null float64 11 carwidth 205 non-null float64 12 carheight 205 non-null float64 13 curbweight 205 non-null int64 14 enginetype 205 non-null object 15 cylindernumber 205 non-null int64 16 enginesize 205 non-null int64 17 fuelsystem 205 non-null object 18 boreratio 205 non-null float64 19 stroke 205 non-null float64 20 compressionratio 205 non-null float64 21 horsepower 205 non-null int64 22 peakrpm 205 non-null int64 23 citympg 205 non-null int64 24 highwaympg 205 non-null int64 25 price 205 non-null float64 26 brand 205 non-null object 27 model 205 non-null object dtypes: float64(8), int64(10), object(10) memory usage: 45.0+ KB
Originale Daten sichern, Originale Daten werden für machinelles Lernen wieder gebraucht¶
# originale Daten sichern, für weitere Modellevariante
df_cp= df.copy(deep= True)
df
| car_ID | symboling | CarName | fueltype | aspiration | doornumber | carbody | drivewheel | enginelocation | wheelbase | carlength | carwidth | carheight | curbweight | enginetype | cylindernumber | enginesize | fuelsystem | boreratio | stroke | compressionratio | horsepower | peakrpm | citympg | highwaympg | price | brand | model | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 3 | alfa-romero giulia | gas | std | 2 | convertible | rwd | front | 88.6 | 168.8 | 64.1 | 48.8 | 2548 | dohc | 4 | 130 | mpfi | 3.47 | 2.68 | 9.0 | 111 | 5000 | 21 | 27 | 13495.0 | alfa-romero | giulia |
| 1 | 2 | 3 | alfa-romero stelvio | gas | std | 2 | convertible | rwd | front | 88.6 | 168.8 | 64.1 | 48.8 | 2548 | dohc | 4 | 130 | mpfi | 3.47 | 2.68 | 9.0 | 111 | 5000 | 21 | 27 | 16500.0 | alfa-romero | stelvio |
| 2 | 3 | 1 | alfa-romero Quadrifoglio | gas | std | 2 | hatchback | rwd | front | 94.5 | 171.2 | 65.5 | 52.4 | 2823 | ohcv | 6 | 152 | mpfi | 2.68 | 3.47 | 9.0 | 154 | 5000 | 19 | 26 | 16500.0 | alfa-romero | Quadrifoglio |
| 3 | 4 | 2 | audi 100 ls | gas | std | 4 | sedan | fwd | front | 99.8 | 176.6 | 66.2 | 54.3 | 2337 | ohc | 4 | 109 | mpfi | 3.19 | 3.40 | 10.0 | 102 | 5500 | 24 | 30 | 13950.0 | audi | 100 ls |
| 4 | 5 | 2 | audi 100ls | gas | std | 4 | sedan | 4wd | front | 99.4 | 176.6 | 66.4 | 54.3 | 2824 | ohc | 5 | 136 | mpfi | 3.19 | 3.40 | 8.0 | 115 | 5500 | 18 | 22 | 17450.0 | audi | 100ls |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 200 | 201 | -1 | volvo 145e (sw) | gas | std | 4 | sedan | rwd | front | 109.1 | 188.8 | 68.9 | 55.5 | 2952 | ohc | 4 | 141 | mpfi | 3.78 | 3.15 | 9.5 | 114 | 5400 | 23 | 28 | 16845.0 | volvo | 145e (sw) |
| 201 | 202 | -1 | volvo 144ea | gas | turbo | 4 | sedan | rwd | front | 109.1 | 188.8 | 68.8 | 55.5 | 3049 | ohc | 4 | 141 | mpfi | 3.78 | 3.15 | 8.7 | 160 | 5300 | 19 | 25 | 19045.0 | volvo | 144ea |
| 202 | 203 | -1 | volvo 244dl | gas | std | 4 | sedan | rwd | front | 109.1 | 188.8 | 68.9 | 55.5 | 3012 | ohcv | 6 | 173 | mpfi | 3.58 | 2.87 | 8.8 | 134 | 5500 | 18 | 23 | 21485.0 | volvo | 244dl |
| 203 | 204 | -1 | volvo 246 | diesel | turbo | 4 | sedan | rwd | front | 109.1 | 188.8 | 68.9 | 55.5 | 3217 | ohc | 6 | 145 | idi | 3.01 | 3.40 | 23.0 | 106 | 4800 | 26 | 27 | 22470.0 | volvo | 246 |
| 204 | 205 | -1 | volvo 264gl | gas | turbo | 4 | sedan | rwd | front | 109.1 | 188.8 | 68.9 | 55.5 | 3062 | ohc | 4 | 141 | mpfi | 3.78 | 3.15 | 9.5 | 114 | 5400 | 19 | 25 | 22625.0 | volvo | 264gl |
205 rows × 28 columns
Festlegung von kategorischen und numerischen Spalten für ML¶
# Define categorical and numerical columns
categorical_columns = ['fueltype', 'aspiration', 'carbody', 'drivewheel',
'enginelocation', 'enginetype', 'fuelsystem', 'brand', 'model']
numerical_columns = ['symboling', 'doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight',
'curbweight', 'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
'peakrpm', 'citympg', 'highwaympg']
print(df["fueltype"].unique(), df["aspiration"].unique(), df["carbody"].unique(), df["drivewheel"].unique(), df["enginelocation"].unique(),
df["enginetype"].unique(), df["fuelsystem"].unique())
print(df["enginesize"].unique())
['gas' 'diesel'] ['std' 'turbo'] ['convertible' 'hatchback' 'sedan' 'wagon' 'hardtop'] ['rwd' 'fwd' '4wd'] ['front' 'rear'] ['dohc' 'ohcv' 'ohc' 'l' 'rotor' 'ohcf' 'dohcv'] ['mpfi' '2bbl' 'mfi' '1bbl' 'spfi' '4bbl' 'idi' 'spdi'] [130 152 109 136 131 108 164 209 61 90 98 122 156 92 79 110 111 119 258 326 91 70 80 140 134 183 234 308 304 97 103 120 181 151 194 203 132 121 146 171 161 141 173 145]
4. Modeling (ML-Maschinelles Lernen)¶
Mathematische Grundlage: die Kleinste-Quadrate-Methode
4.1 Der allgemeine lineare Fall¶
- model = LinearRegression()
- model.fit(X_train, y_train)
4.1.1 Ein eindimensionles Linear-Regression:¶
y= a*x + b; x, y: Messwerte; a: Steigerung, b: bias
4.1.2 N-dimensionles Linear-Regression:¶
die Kleinste-Quadrate-Methode(KQM)¶
r= Xα - y;¶
m x (n+1) ; m: Anzahl der Beobachtungen(oder Anzahl der Zeilen in einer Spalte)
a=(a0, a1, a2,... an), n+1-Dimension, Unbekannte; a0: bias
X: mx(n+1) dimensionale Matrix (Beobachtungen-> Xi,j: i Zeile; j: Spalte
y: m-Messwerte (Preise)
Ziel: die Summe durch KQM zu minimieren
Die Lösung: np.linalg.inv()
⚠️ Achtung¶
Bedingung für eindeutige Lösung
Damit die Matrix $A^T A$ stabil invertierbar ist, muss die Anzahl der Zeilen (Beobachtungen) stets größer sein als die Anzahl der Spalten (Variablen/Features).
Ist das nicht der Fall, existiert keine eindeutige Lösung für das Gleichungssystem.Lineare Unabhängigkeit der Spalten
Werden neue Spalten aus bestehenden Daten erzeugt, dürfen sie nicht in einem proportionalen Verhältnis zu anderen Spalten stehen. Andernfalls sind die Spalten linear abhängig, und die Matrix (A^T A) wird singulär (nicht invertierbar).- In diesem Fall liefert
np.linalg.inv()keine echte Inverse, sondern bestenfalls eine Pseudo-Inverse (z. B. übernp.linalg.pinv()), was mathematisch problematisch ist. - Beispiel:
- $x_1 = a \cdot x$ → ❌ nicht erlaubt (linear abhängig)
- $x_1 = \sqrt{x}$ → ✅ erlaubt (nicht linear abhängig)
- In diesem Fall liefert
4.2 Verfahren von Encoding:¶
Im Rahmen des Projektes werden bei Encoding für katogorischen Daten zwei Verfahren angewendet: One-Hot und Ordinal Verfahren. das lässt sich am besten mit Beispielen erklären:
📌 Beispiel: Encoding von Kategorien¶
zwei Kategorien:
- carbody:
["sedan", "hatchback", "wagon", "convertible", "hardtop"] - enginetype:
["ohc", "l", "dohc", "dohcv", "ohcf", "ohcv", "rotor"]
Beispieldaten:
| carbody | enginetype |
|---|---|
| sedan | ohc |
| hatchback | l |
| wagon | dohc |
| convertible | dohcv |
| hardtop | ohcv |
| sedan | rotor |
🔹 1. Label Encoding¶
from sklearn.preprocessing import LabelEncoder
le_carbody = LabelEncoder()
le_enginetype = LabelEncoder()
Jede Kategorie wird durch eine Ganzzahl (sortiert nach der Bezeichnung, z.B. ['Z', 'G', 'B'], nach Sortierung: ['B','G','Z'], B=0, G=1, Z=2 dargestellt.
Beispielausgabe:
- Carbody classes:
['convertible', 'hardtop', 'hatchback', 'sedan', 'wagon'] - Enginetype classes:
['dohc', 'dohcv', 'l', 'ohc', 'ohcf', 'ohcv', 'rotor']
| carbody | enginetype | carbody_label | enginetype_label |
|---|---|---|---|
| sedan | ohc | 3 | 3 |
| hatchback | l | 2 | 2 |
| wagon | dohc | 4 | 0 |
| convertible | dohcv | 0 | 1 |
| hardtop | ohcv | 1 | 5 |
| sedan | rotor | 3 | 6 |
🔹 2. One-Hot Encoding¶
Jede Kategorie wird in eine eigene Spalte mit 0/1 umgewandelt. from sklearn.preprocessing import OneHotEncoder
- ohe = OneHotEncoder(sparse=False)
- encoded = ohe.fit_transform(df[["carbody", "enginetype"]])
Beispielausgabe:
| carbody_convertible | carbody_hardtop | carbody_hatchback | carbody_sedan | carbody_wagon | enginetype_dohc | enginetype_dohcv | enginetype_l | enginetype_ohc | enginetype_ohcf | enginetype_ohcv | enginetype_rotor |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
✅ Vergleich: LabelEncoder vs. OneHotEncoder¶
| Kriterium | Label Encoding | One-Hot Encoding |
|---|---|---|
| Darstellung | Jede Kategorie → eine Zahl | Jede Kategorie → eigene Spalte mit 0/1 |
| Speicherbedarf | Sehr gering (nur eine Spalte) | Höher (eine Spalte pro Kategorie) |
| Interpretation | Problematisch: Modell denkt, Zahlen hätten Reihenfolge | Klar & eindeutig, keine künstliche Reihenfolge |
| Gut für | Entscheidungsbäume, Random Forest | Lineare Modelle, Neuronale Netze |
| Schlecht für | Lineare Modelle (da künstliche Rangordnung) | Viele Kategorien (führt zu sehr breiten Matrizen) |
⚠️Achtung: ich benutze in meinem Code: pd.get_dummies(drop_first=True). man muss ganz klar den Unterschied zu OneHotEncoder() verstehen.¶
- ohe = OneHotEncoder(sparse=False)
- encoded = ohe.fit_transform(df[["carbody", "enginetype"]])
- um das gleiche wie pd.get_dummies() zu erreichen: df_encoded = pd.DataFrame(encoded, columns=ohe.get_feature_names_out(["carbody","enginetype"])
- folgender Vergleich hift:
| Aspekt | OneHotEncoder (sklearn) | pd.get_dummies() (pandas) |
|---|---|---|
| Output | NumPy-Array oder DataFrame (mit Zusatzschritt) | Direkt ein Pandas-DataFrame |
| Wiederverwendbar | ✅ Ja (fit/transform für neue Daten) | ❌ Nein (funktioniert nur auf vorhandenen Daten) |
| Integration in Pipeline | ✅ Sehr gut (für ML-Workflows in sklearn) | ❌ Nicht vorgesehen |
| Drop-First Option | Standardmäßig ❌ (aber man kann drop="first" setzen) |
✅ drop_first=True verfügbar |
| Einfache Anwendung | etwas mehr Code (fit/transform nötig) | sehr einfach, 1 Zeile |
👉 Fazit:
- Label Encoding ist kompakt, kann aber problematisch sein, wenn das Modell Zahlen als "Rangordnung" interpretiert.
- One-Hot Encoding vermeidet dieses Problem, erzeugt aber mehr Spalten und damit größere Datensätze.
In der Projektarbeit werden folgende Verfahren untersucht:
- Kategorische Spalten als One-Hot-Werte + numerische Spalten (standardisiert) → lineares Modell
- Kategorische Spalten als Ordinalwerte + numerische Spalten (standardisiert) → lineares Modell
- Kategorische Spalten als Ordinalwerte + numerische Spalten (standardisiert) + Ausreißer behandeln → lineares Modell
- Aufbereitung von highwaympg: limit_highwaympg_r=52
- Wenn highwaympg > 52, die Zeile wird als Ausreißer gelöscht. Es gibt zwei Ausreißer
- Kategorische Spalten als Ordinalwerte + numerische Spalten (standardisiert) + neue Spalten durch $x_1 = \sqrt{x}$, $x_2 = x^2$
Funktion für Modelltraining: training_carmodel(df_cp, categorical_columns, numerical_columns,encoding_mode=2, test_daten_size=0.2, squared_cols=0)
squared_cols: 0, 1, 2; 0: keine neue Spalten;
- 1: $x_1 = \sqrt{x}$ ; falls $x < 0 \;\Rightarrow\; x_1 = -\sqrt{|x|}$
- 2: $x_2 = x^2$
was bedeutet "standardisiert"?
StandardScaler() ist ein Werkzeug aus scikit-learn, das numerische Features standardisiert: Es bringt alle angegebenen Spalten (Features) auf eine gemeinsame Skala, indem jeder Wert so transformiert wird:
z = x−μσ
z = σx−μ
Dabei ist x der Originalwert, μ der Mittelwert und σ die Standardabweichung der jeweiligen Spalte.
fit_transform() berechnet zuerst Mittelwert und Standardabweichung für jede numerische Spalte – und transformiert dann alle Werte entsprechend.
Das Ergebnis: Alle Werte der angegebenen numerischen Spalten haben danach Mittelwert=0 und Standardabweichung=1. Die Skalierung ist jetzt vergleichbar, egal ob z.B. Einkommen, Alter oder Wohnfläche vorher unterschiedlich große Zahlen waren.
4.3 Funktionsdefinition für Modelltraining und Vorhersage:¶
4.3.1 training_carmodel() + predict_price()¶
def training_carmodel(df_cp, categorical_columns, numerical_columns,encoding_mode=2, test_daten_size=0.2, squared_cols=0):
def predict_price(df_test, model, label_encoders, scaler, categorical_columns, numerical_columns,squared_cols=0):
df_cp: data Frame¶
categorical_columns = ['fueltype', 'aspiration', 'carbody', 'drivewheel','enginelocation', 'enginetype', 'fuelsystem', 'brand', 'model']¶
numerical_columns = ['symboling', 'doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight','curbweight', 'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower','peakrpm', 'citympg', 'highwaympg']¶
encoding_mode:¶
- =1: One-Hot Encoder;
- =2: Ordinal Encoder;
test_daten_size:¶
- =0.2: 20% der Daten als Testdaten, 80% als Trainingsdaten
squared_cols: 0, 1, 2; 0: keine neue Spalten; für für numerische Spalten¶
- 1: $x_1 = \sqrt{x}$ ; falls $x < 0 \;\Rightarrow\; x_1 = -\sqrt{|x|}$
- 2: $x_2 = x^2$
4.3.2 Voraussetzung: categorical_columns und numerical_columns sind definiert¶
- categorical_columns = ['fueltype', 'aspiration', 'carbody', 'drivewheel', 'enginelocation', 'enginetype', 'fuelsystem', 'brand', 'model']
- numerical_columns = ['symboling', 'doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight', 'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower', 'peakrpm', 'citympg', 'highwaympg']
4.3.3 Dokumentation: Scaling & Ordinal-Encoding für ML¶
1️⃣ Numerische Werte: StandardScaler¶
Beim maschinellen Lernen ist es üblich, numerische Features zu standardisieren, damit sie vergleichbare Skalen haben. Dies verbessert die Stabilität und Geschwindigkeit vieler Modelle.
Training¶
scaler = StandardScaler()
df_cp[numerical_columns] = scaler.fit_transform(df_cp[numerical_columns])
fit_transform()berechnet Mittelwert (mean) und Standardabweichung (std) und wendet sie an: (x_{scaled} = \frac{x - mean}{std})- Die Mittelwerte und Standardabweichungen werden im
scalergespeichert.
Vorhersage / neue Daten¶
df_test[numerical_columns] = scaler.transform(df_test[numerical_columns])
- Nur
transform()wird verwendet, um die gleiche Skalierung wie beim Training zu garantieren. - Nie
fit_transform()auf neuen Daten verwenden!
2️⃣ Kategoriale Werte: Ordinal-Encoding (LabelEncoder)¶
Training¶
label_encoders = {}
for column in categorical_columns:
le = LabelEncoder()
df_cp[column] = le.fit_transform(df_cp[column])
label_encoders[column] = le
- Für jede Spalte ein eigener Encoder.
fit_transform()lernt die Kategorien und wandelt sie in Zahlen um.- Die Encoder werden gespeichert, um bei Vorhersagen dieselbe Kodierung zu nutzen.
Vorhersage / neue Daten¶
if label_encoders is not None:
for column in categorical_columns:
df_test[column] = label_encoders[column].transform(df_test[column])
- Die Spalten werden mit den gespeicherten Encodern transformiert.
- Neue, unbekannte Kategorien führen zu einem Fehler.
3 ✅ Zusammenfassung¶
| Feature-Typ | Training | Vorhersage | Warum wichtig |
|---|---|---|---|
| Numerisch | fit_transform() |
transform() |
Konsistente Standardisierung |
| Kategorial (Ordinal) | fit_transform() + Encoder speichern |
transform() mit gespeichertem Encoder |
Konsistente Kodierung |
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
import pandas as pd
import numpy as np
def training_carmodel(df_cp, categorical_columns, numerical_columns,
encoding_mode=2, test_daten_size=0.2, squared_cols=0):
"""
Trainiert ein lineares Regressionsmodell auf Auto-Daten.
Parameters:
-----------
df_cp : pd.DataFrame
Trainingsdaten inklusive Zielspalte 'price'.
categorical_columns : list
Liste der kategorialen Spalten.
numerical_columns : list
Liste der numerischen Spalten.
encoding_mode : int
1 = One-Hot Encoding, 2 = Ordinal (Label) Encoding
test_daten_size : float
Anteil der Daten für den Testsplit.
squared_cols : int
0 = keine Transformation, 1 = sqrt Transformation, 2 = quadrieren
Returns:
--------
model : LinearRegression
Trainiertes Regressionsmodell
label_encoders : dict
Dictionary mit LabelEncodern für jede kategoriale Spalte (nur bei encoding_mode=2)
scaler : StandardScaler
Scaler für numerische Spalten
mse : float
Mean Squared Error auf Testdaten
r2_square : float
R^2 Score auf Testdaten
pred_df : pd.DataFrame
Vergleich von tatsächlichen und vorhergesagten Preisen
"""
# ----------------------
# Encoding
# ----------------------
if encoding_mode == 1:
df_cp = pd.get_dummies(df_cp, columns=categorical_columns, drop_first=True)
label_encoders = None
else:
label_encoders = {}
for column in categorical_columns:
le = LabelEncoder()
df_cp[column] = le.fit_transform(df_cp[column])
label_encoders[column] = le
# ----------------------
# Feature Engineering
# ----------------------
df_cp['power_to_weight_ratio'] = df_cp['horsepower'] / df_cp['curbweight']
#df_cp['log_enginesize'] = np.log(df_cp['enginesize'] + 1.2)
for column in numerical_columns:
if squared_cols == 2: #x^2
df_cp[f'{column}_squared'] = df_cp[column] ** 2
elif squared_cols == 1: # sqrt(x)
df_cp[f'{column}_squared'] = np.where(
df_cp[column] <= 0,
-np.sqrt(abs(df_cp[column])),
np.sqrt(abs(df_cp[column]))
)
# ----------------------
# Scaling
# ----------------------
scaler = StandardScaler()
df_cp[numerical_columns] = scaler.fit_transform(df_cp[numerical_columns])
# ----------------------
# Train-Test-Split
# ----------------------
X = df_cp.drop(['price', 'car_ID', 'CarName'], axis=1)
y = df_cp['price']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_daten_size, random_state=42)
# print("Rows:", len(df_cp), "columns", len(df_cp.columns) )
# ----------------------
# Modelltraining
# ----------------------
model = LinearRegression()
model.fit(X_train, y_train)
# ----------------------
# Vorhersagen
# ----------------------
y_pred = model.predict(X_test)
# ----------------------
# Modellbewertung
# ----------------------
mse = mean_squared_error(y_test, y_pred)
r2_square = r2_score(y_test, y_pred)
pred_df = pd.DataFrame({
'Actual Value': y_test,
'Predicted Value': y_pred,
'Difference': y_test - y_pred,
'rel%': abs((y_test - y_pred)) / y_test * 100
})
#print(f"r2_score: {r2_score:.4f}, MSE: {mse:.2f}")
return model, label_encoders, scaler, mse, r2_square, pred_df
def predict_price(df_test, model, label_encoders, scaler, categorical_columns, numerical_columns,
squared_cols=0):
"""
Macht Vorhersagen für neue Auto-Daten basierend auf einem trainierten Modell.
Parameters:
-----------
df_test : pd.DataFrame
Neue Daten zum Vorhersagen
model : LinearRegression
Trainiertes Modell
label_encoders : dict
LabelEncoder pro kategorialer Spalte (nur bei Ordinal-Encoding)
scaler : StandardScaler
Scaler für numerische Spalten
categorical_columns : list
Liste der kategorialen Spalten
numerical_columns : list
Liste der numerischen Spalten
squared_cols : int
0 = keine Transformation, 1 = sqrt Transformation, 2 = quadrieren
Returns:
--------
y_pred : np.ndarray
Vorhergesagte Preise
"""
# ----------------------
# Encoding
# ----------------------
if label_encoders is not None:
for column in categorical_columns:
df_test[column] = label_encoders[column].transform(df_test[column])
# ----------------------
# Feature Engineering
# ----------------------
df_test['power_to_weight_ratio'] = df_test['horsepower'] / df_test['curbweight']
#df_test['log_enginesize'] = np.log(df_test['enginesize'] + 1.2)
for column in numerical_columns:
if squared_cols == 2:
df_test[f'{column}_squared'] = df_test[column] ** 2
elif squared_cols == 1:
df_test[f'{column}_squared'] = np.where(
df_test[column] <= 0,
-np.sqrt(abs(df_test[column])),
np.sqrt(abs(df_test[column]))
)
# ----------------------
# Scaling
# ----------------------
df_test[numerical_columns] = scaler.transform(df_test[numerical_columns])
# ----------------------
# Features für Prediction vorbereiten
# ----------------------
drop_cols = ['car_ID', 'CarName']
if 'price' in df_test.columns:
drop_cols.append('price')
X_new = df_test.drop(drop_cols, axis=1)
# ----------------------
# Prediction
# ----------------------
y_pred = model.predict(X_new)
return y_pred
4.3.4 Erklärung von r2_score(y_test, y_pred)¶
R² (Bestimmtheitsmaß) - Erklärung
1️⃣ Definition von R² (Bestimmtheitsmaß)¶
Für ein Test-Set gilt:
R² = 1 - (Σ(y_i - ŷ_i)² / Σ(y_i - ȳ)²)¶
- y_i = echte Werte¶
- ŷ_i = vorhergesagte Werte¶
- ȳ = Mittelwert der echten Werte¶
Interpretation:
- R² = 1 → perfekte Vorhersage
- R² = 0 → Modell sagt genauso gut wie Mittelwert
- R² < 0 → Modell ist schlechter als Mittelwert
2️⃣ Wann wird R² negativ?¶
Σ(y_i - ŷ_i)² > Σ(y_i - ȳ)²¶
- Das passiert, wenn dein Modell die Werte so schlecht vorhersagt, dass der Mittelwert eine bessere Vorhersage liefern würde.
- Typische Ursachen:
- Lineares Modell auf nichtlinearen Zusammenhängen
- Zu wenige oder falsche Features
- Ausreißer im Test-Set
- Übermäßiges Rauschen in den Daten
Negatives R² ist ein Warnsignal, dass dein Modell nicht gut passt. Es bedeutet nicht, dass der Wert falsch berechnet wurde.
4.4 Methodische Untersuchung für das Modelltraining¶
4.4.1 Kategorische Spalten mit One-Hot-Encoding + numerische Spalten (standardisiert) → lineares Modell¶
#One-Hot Encoder
df_cp= df.copy(deep= True)
#training_carmodel(1, test_daten_size=0.2, squared_cols=0)
model, label_encoders, scaler, mse, r2_square, pred_df= training_carmodel(df_cp, categorical_columns, numerical_columns, 1, test_daten_size=0.2, squared_cols=0)
print(f"r2_score: {r2_square:.4f}, MSE: {mse:.2f}")
print("rel. min.:",pred_df['rel%'].min(), "rel. max.:",pred_df['rel%'].max(), "rel. mean.:",pred_df['rel%'].mean())
print('\nHistogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage')
fig = px.histogram(pred_df, x='rel%', nbins=30, height=600,
title='Histogramm von rel%',
labels={'rel%': 'Rel. Preis-Diff in %'})
fig.show()
fig1 = px.scatter(
pred_df,
x='Actual Value', # oder 'y_test', je nachdem wie deine Spalte heißt
y='rel%',
title='Scatterplot: y_test vs. rel%',
labels={'Actual Value': 'y_test (echte Werte)', 'rel%': 'Relative Abweichung [%]'},
opacity=0.7
)
fig1.show()
r2_score: 0.7505, MSE: 19693826.14 rel. min.: 1.466256502843084 rel. max.: 86.35137160481213 rel. mean.: 31.82627067379519 Histogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage
4.4.2 Kategorische Spalten mit Ordinal-Encoding + numerische Spalten (standardisiert) → lineares Modell¶
#Ordinal Encoder
#categorical colums as ordinal value + numerical columns (standardisiert) -> Linear model
df_cp= df.copy(deep= True)
# mse, r2_square, model, pred_df= training_carmodel(df_cp,2, test_daten_size=0.2,squared_cols=0)
model, label_encoders, scaler, mse, r2_square, pred_df= training_carmodel(df_cp, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0)
print(f"r2_score: {r2_square:.4f}, MSE: {mse:.2f}")
print("rel. min.:",pred_df['rel%'].min(), "rel. max.:",pred_df['rel%'].max(), "rel. mean.:",pred_df['rel%'].mean())
print('\nHistogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage')
fig = px.histogram(pred_df, x='rel%', nbins=30, height=600,
title='Histogramm von rel%',
labels={'rel%': 'Rel. Preis-Diff in %'})
fig.show()
fig1 = px.scatter(
pred_df,
x='Actual Value', # oder 'y_test', je nachdem wie deine Spalte heißt
y='rel%',
title='Scatterplot: y_test vs. rel%',
labels={'Actual Value': 'y_test (echte Werte)', 'rel%': 'Relative Abweichung [%]'},
opacity=0.7
)
fig1.show()
r2_score: 0.8660, MSE: 10580608.45 rel. min.: 0.21915016785090702 rel. max.: 72.76697806763775 rel. mean.: 16.964314923719503 Histogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage
Das Ergebnis zeigt dass bei One-Hot Encoding schlechter ist als bei Label-Encoding.¶
Das ist ein sehr kompliziertes Thema. hier hift KI zu erklären.
Meine Frage:¶
In meinen Daten habe ich mehrere kategorische Spalten, wobei jede Spalte eine unterschiedliche Anzahl von Kategorien enthält.
Beispiele:
fueltype:['gas', 'diesel']carbody:['convertible', 'hardtop', 'hatchback', 'sedan', 'wagon']
Beim Modelltraining habe ich pd.get_dummies(drop_first=True) verwendet und festgestellt, dass die Performance des Modells mit One-Hot-Encoding schlechter ist als mit Label-Encoding.
Meine Frage: Liegt das schlechtere Ergebnis daran, dass
- die Anzahl der Kategorien zwischen den verschiedenen Spalten stark variiert,
- oder an der Verwendung von
drop_first=True, - oder daran, dass die maximale Anzahl der Kategorien relativ klein ist (z. B. ≤10), sodass Label-Encoding, obwohl die Kategorien einfach nach Zahlen sortiert werden, gut funktioniert, da die Unterschiede zwischen den Zahlen nicht zu groß sind (z. B. 1, 2, 3, …, 8 und nicht 1 bis 20)?
kann du meine Formulierung besser organisieren und verbessern
Antwort:¶
Analyse: One-Hot-Encoding vs. Label-Encoding bei unterschiedlichen Kategorien¶
Du hast festgestellt, dass One-Hot-Encoding schlechter performt als Label-Encoding. Hier ist eine systematische Analyse.
1️⃣ Einfluss der unterschiedlichen Anzahl von Kategorien¶
Beispiele:
fueltype→ 2 Kategorien (gas,diesel)carbody→ 5 Kategorien (convertible, …,wagon)- andere Spalten evtl. bis 10 Kategorien
Problem bei One-Hot-Encoding:
- Jede Kategorie wird in eine eigene Dummy-Spalte umgewandelt.
- Modelle wie lineare Regression oder neuronale Netze erhalten dadurch sehr unterschiedliche Dimensionen.
- Spalten mit vielen Dummy-Spalten können dominieren, während kleine Spalten unterrepräsentiert bleiben.
Für Baummodelle (DecisionTree, RandomForest) ist das weniger problematisch.
2️⃣ Einfluss von drop_first=True¶
drop_first=Trueentfernt die erste Kategorie jeder Spalte, um Multikollinearität zu vermeiden.- Beispiel:
carbodymit 5 Kategorien → nur 4 Dummy-Spalten bleiben. - Folge: Das Modell muss die Relation zur ausgelassenen Kategorie implizit lernen.
- Bei kleinen Datensätzen oder spärlicher Verteilung kann das die Leistung verschlechtern.
3️⃣ Einfluss der Anzahl der Kategorien bei Label-Encoding¶
Label-Encoding wandelt jede Kategorie in eine Zahl um, z. B.:
carbody: convertible=0, hardtop=1, hatchback=2, sedan=3, wagon=4
Bei kleiner Kategorieanzahl (2–10):
Unterschiede zwischen den Zahlen sind klein.
Lineare Modelle oder neuronale Netze können die Unterschiede leicht lernen.
Label-Encoding ist kompakt und oft stabiler.
Bei vielen Kategorien (z. B. 50–100) kann Label-Encoding problematisch sein, weil das Modell eine falsche Rangordnung annimmt.
4️⃣ Zusammenfassung¶
| Faktor | Effekt auf Modellleistung |
|---|---|
| One-Hot, unterschiedliche Anzahl Kategorien | Spalten mit vielen Dummies dominieren; kleine Spalten werden schwach gewichtet |
| drop_first=True | Kann bei kleinen Datensätzen Informationsverlust erzeugen |
| Label-Encoding, wenige Kategorien | Modell kann Rangordnung leicht lernen; Zahlenunterschied klein → kompakter und stabiler |
🔹 Fazit¶
- Dass Label-Encoding besser performt, liegt an einer Kombination von Faktoren:
- Drop_first → kleine Informationsverluste
- One-Hot-Matrix wird bei ungleichen Kategoriespalten sehr breit → schwer für Modell zu lernen
- Label-Encoding mit wenigen Kategorien → Modell kann Unterschiede gut interpretieren
- Empfehlung:
- Lineare Modelle / kleine Datensätze: Label-Encoding ist oft besser.
- Neuronale Netze oder Random Forests: One-Hot ist sinnvoll, ggf. drop_first=False verwenden und Feature-Skalierung beachten.
#Ordinal Encoder
df_cp= df.copy(deep= True)
# entfernen die Zeilen wo df['highwaympg'] >= limit_highwaympg_r]
df_cp= df_cp.loc[df_cp['highwaympg'] < limit_highwaympg_r]
model, label_encoders, scaler, mse, r2_square, pred_df= training_carmodel(df_cp, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0)
print(f"r2_score: {r2_square:.4f}, MSE: {mse:.2f}")
print("rel. min.:",pred_df['rel%'].min(), "rel. max.:",pred_df['rel%'].max(), "rel. mean.:",pred_df['rel%'].mean())
print('\nHistogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage')
fig = px.histogram(pred_df, x='rel%', nbins=30, height=600,
title='Histogramm von rel%',
labels={'rel%': 'Rel. Preis-Diff in %'})
fig.show()
fig1 = px.scatter(
pred_df,
x='Actual Value', # oder 'y_test', je nachdem wie deine Spalte heißt
y='rel%',
title='Scatterplot: y_test vs. rel%',
labels={'Actual Value': 'y_test (echte Werte)', 'rel%': 'Relative Abweichung [%]'},
opacity=0.7
)
fig1.show()
r2_score: 0.8771, MSE: 12637217.35 rel. min.: 1.7650865199468755 rel. max.: 57.73533475352176 rel. mean.: 15.149582505329542 Histogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage
import joblib
# Speichern
joblib.dump(model, "linear_model_CarPricePred.pkl")
['linear_model_CarPricePred.pkl']
4.4.4 Kategorische Spalten mit Ordinal-Encoding + numerische Spalten (standardisiert) + Transformation durch sqrt() oder x^2¶
modellieren nicht linearen Anteil von numerische Spalten durch zusätzliche Spalten
- squared_cols=1: sqrt(x) ; wenn x< 0 , -sqrt(abs(x)
- squared_cols=2: x^2
#Ordinal Encoder
df_cp= df.copy(deep= True)
df_cp= df_cp.loc[df_cp['highwaympg'] < limit_highwaympg_r]
model, label_encoders, scaler, mse, r2_square, pred_df= training_carmodel(df_cp, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=1)
print(f"r2_score: {r2_square:.4f}, MSE: {mse:.2f}")
print("rel. min.:",pred_df['rel%'].min(), "rel. max.:",pred_df['rel%'].max(), "rel. mean.:",pred_df['rel%'].mean())
print('\nHistogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage')
fig = px.histogram(pred_df, x='rel%', nbins=30, height=600,
title='Histogramm von rel%',
labels={'rel%': 'Rel. Preis-Diff in %'})
fig.show()
fig1 = px.scatter(
pred_df,
x='Actual Value', # oder 'y_test', je nachdem wie deine Spalte heißt
y='rel%',
title='Scatterplot: y_test vs. rel%',
labels={'Actual Value': 'y_test (echte Werte)', 'rel%': 'Relative Abweichung [%]'},
opacity=0.7
)
fig1.show()
r2_score: 0.8919, MSE: 11117848.75 rel. min.: 0.6066636862227491 rel. max.: 41.78182275926226 rel. mean.: 14.708128895858007 Histogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage
df_cp= df.copy(deep= True)
#df_cp.info()
#df= df.loc[df['highwaympg'] < 52 ]
#df= df.loc[df['highwaympg'] < limit_highwaympg_r] # df is cleaned for 'highwaympg'] < limit_highwaympg_r
#df.info()
#df.cylindernumber.unique()
print("fueltype:",df_cp['fueltype'].unique())
print("fuelsystem:",df_cp['fuelsystem'].unique())
print("carbody:",df_cp['carbody'].unique())
print("drivewheel:",df_cp['drivewheel'].unique())
print("enginelocation:", df_cp['enginelocation'].unique())
print("enginetype:",df_cp['enginetype'].unique())
print("brand:",df_cp['brand'].unique())
print("model:",df_cp['model'].unique())
#categorical_columns = ['fueltype', 'aspiration', 'carbody', 'drivewheel','enginelocation', 'enginetype', 'fuelsystem', 'brand', 'model']
fueltype: ['gas' 'diesel'] fuelsystem: ['mpfi' '2bbl' 'mfi' '1bbl' 'spfi' '4bbl' 'idi' 'spdi'] carbody: ['convertible' 'hatchback' 'sedan' 'wagon' 'hardtop'] drivewheel: ['rwd' 'fwd' '4wd'] enginelocation: ['front' 'rear'] enginetype: ['dohc' 'ohcv' 'ohc' 'l' 'rotor' 'ohcf' 'dohcv'] brand: ['alfa-romero' 'audi' 'bmw' 'chevrolet' 'dodge' 'honda' 'isuzu' 'jaguar' 'maxda' 'mazda' 'buick' 'mercury' 'mitsubishi' 'Nissan' 'nissan' 'peugeot' 'plymouth' 'porsche' 'porcshce' 'renault' 'saab' 'subaru' 'toyota' 'toyouta' 'vokswagen' 'volkswagen' 'vw' 'volvo'] model: ['giulia' 'stelvio' 'Quadrifoglio' '100 ls' '100ls' 'fox' '5000' '4000' '5000s (diesel)' '320i' 'x1' 'x3' 'z4' 'x4' 'x5' 'impala' 'monte carlo' 'vega 2300' 'rampage' 'challenger se' 'd200' 'monaco (sw)' 'colt hardtop' 'colt (sw)' 'coronet custom' 'dart custom' 'coronet custom (sw)' 'civic' 'civic cvcc' 'accord cvcc' 'accord lx' 'civic 1500 gl' 'accord' 'civic 1300' 'prelude' 'civic (auto)' 'MU-X' 'D-Max ' 'D-Max V-Cross' 'xj' 'xf' 'xk' 'rx3' 'glc deluxe' 'rx2 coupe' 'rx-4' '626' 'glc' 'rx-7 gs' 'glc 4' 'glc custom l' 'glc custom' 'electra 225 custom' 'century luxus (sw)' 'century' 'skyhawk' 'opel isuzu deluxe' 'skylark' 'century special' 'regal sport coupe (turbo)' 'cougar' 'mirage' 'lancer' 'outlander' 'g4' 'mirage g4' 'montero' 'pajero' 'versa' 'gt-r' 'rogue' 'latio' 'titan' 'leaf' 'juke' 'note' 'clipper' 'nv200' 'dayz' 'fuga' 'otti' 'teana' 'kicks' '504' '304' '504 (sw)' '604sl' '505s turbo diesel' 'fury iii' 'cricket' 'satellite custom (sw)' 'fury gran sedan' 'valiant' 'duster' 'macan' 'panamera' 'cayenne' 'boxter' '12tl' '5 gtl' '99e' '99le' '99gle' 'dl' 'brz' 'baja' 'r1' 'r2' 'trezia' 'tribeca' 'corona mark ii' 'corona' 'corolla 1200' 'corona hardtop' 'corolla 1600 (sw)' 'carina' 'mark ii' 'corolla' 'corolla liftback' 'celica gt liftback' 'corolla tercel' 'corona liftback' 'starlet' 'tercel' 'cressida' 'celica gt' 'rabbit' '1131 deluxe sedan' 'model 111' 'type 3' '411 (sw)' 'super beetle' 'dasher' 'rabbit custom' '145e (sw)' '144ea' '244dl' '245' '264gl' 'diesel' '246']
5. Evaluation (Bewertung und Interpretation der Ereignisse)¶
5.1 Modellbewertung¶
Es gibt kategorische und numerische Daten. Für Machine-Learning müssen die Kategorie in Zhal umgewandelt werden.
Es sind hier zwei Methden One-Hot Encoding und Ordinal Encoding angewendt.
| Methode | R² Score | MSE | rel. min. | rel. max. | rel. mean. |
|---|---|---|---|---|---|
| One-Hot-Encoding | 0.7505 | 19,693,826.14 | 1.47 | 86.35 | 31.83 |
| Ordinal-Encoding | 0.8660 | 10,580,608.45 | 0.22 | 72.77 | 16.96 |
| Ordinal-Encoding - Ausreißer | 0.8771 | 12,637,217.35 | 1.77 | 57.74 | 15.15 |
| Ordinal-Encoding + Sqrt() | 0.8919 | 11,117,848.75 | 0.61 | 41.78 | 14.71 |
Verschiedene Mothoden für Encoding werden untersucht.
5.1.1 One-Hot-Encoding:¶
- One-Hot-Encoding ist gut, wenn die Kategorien wenige und gleichmäßig verteilt sind
- Wenn die Spalten sehr unterschiedlich viele Kategorien haben, kann das Modell eine Verzerrung bekommen.
Die Spalten haben ungleichmäße Anzahl von Kategorien. Der Wert von R² Score=0.7505 zeigt dass One-Hot-Encoding nicht die gute Wahl ist¶
5.1.2 Ordinal-Encoding:¶
- Ordinal Encoding ist gut, wenn es eine natürliche Reihenfolge der Kategorien (Ordinalskala) gibt Beispiel:
Schulnoten: A > B > C > D > F ( 1,2,3,4,5)¶
Größe: small < medium < large¶
Zufriedenheit: low < medium < high¶
Dann macht es Sinn, sie als Zahlen darzustellen (small=0, medium=1, large=2), weil die Reihenfolge auch eine Bedeutung hat.¶
Ordinal Encoding erzeugt eine künstliche Ordnung (nach Buchstab sortiert) annehmen, die gar nicht existiert. Das beeinträchtigt die Genauigkeit des Modells
In Fuelsystem z.B. gibt es so viele Werte ['mpfi', '2bbl', 'mfi', '1bbl', 'spfi', '4bbl', 'idi', 'spdi']. Diese Werte sind nach Buchstaben sortiert und eine Zahl 1 bis 8 vergeben. Das ist nicht optimal.
Die Lösung wäre: Die Reihenfolge für ['mpfi', '2bbl', 'mfi', '1bbl', 'spfi', '4bbl', 'idi', 'spdi'] durch Bewertung der Relevanz zu Preis ist zu bestimmen und in eine Zahlenskala umzuwandeln.
Das ist eine Aufgabe der fachlichen Bewertung und Datenaufbereitung
Das Ergebnis (R² Score=0.86) zeigt dass es auch ohne Reihenfolgebestimmung besseres Ergbnis als One-Hot Encoding erzielt hat¶
5.1.3 Ordinal-Encoding + Behandlung der Ausreißer:¶
Durch statistische Analyse werden Ausreißer beseitigt. Das Ergebnis(R² Score=0.8771) verbessert sich etwas.¶
5.1.4 Ordinal-Encoding + Sqrt():¶
Das Modell wird mit LinearRegression() trainiert.
Dieses Verfahren setzt voraus, dass die Werte in den Features linear mit dem Zielwert (Preis) zusammenhängen – das wäre der Idealfall.
In der Praxis ist das jedoch selten gegeben. Daher ist es üblich, die numerischen Variablen je nach Datenanalyse mit Transformationen wie log(), sqrt() oder x² zu verändern, um Zusammenhänge besser darzustellen.
Das Beispiel mit der sqrt()-Transformation zeigt deutlich, dass sich die Modellgüte verbessert: der R²-Score steigt auf 0.8919.
5.2 Transformation von kategorialen Merkmale in numerische Werte¶
Folgende Merkmale in der Daten:¶
| Feature | Kategorien |
|---|---|
| fueltype | gas, diesel |
| fuelsystem | mpfi, 2bbl, mfi, 1bbl, spfi, 4bbl, idi, spdi |
| carbody | convertible, hatchback, sedan, wagon, hardtop |
| drivewheel | rwd, fwd, 4wd |
| enginelocation | front, rear |
| enginetype | dohc, ohcv, ohc, l, rotor, ohcf, dohcv |
| brand | alfa-romero, audi, bmw, chevrolet, dodge, honda, isuzu, jaguar, maxda, mazda, buick, mercury, mitsubishi, Nissan, nissan, peugeot, plymouth, porsche, porcshce, renault, saab, subaru, toyota, toyouta, vokswagen, volkswagen, vw, volvo |
model: ['giulia' 'stelvio' 'Quadrifoglio' '100 ls' '100ls' 'fox' '5000' '4000' '5000s (diesel)' '320i' 'x1' 'x3' 'z4' 'x4' 'x5' 'impala' 'monte carlo' 'vega 2300' 'rampage' 'challenger se' 'd200' 'monaco (sw)' 'colt hardtop' 'colt (sw)' 'coronet custom' 'dart custom' 'coronet custom (sw)' 'civic' 'civic cvcc' 'accord cvcc' 'accord lx' 'civic 1500 gl' 'accord' 'civic 1300' 'prelude' 'civic (auto)' 'MU-X' 'D-Max ' 'D-Max V-Cross' 'xj' 'xf' 'xk' 'rx3' 'glc deluxe' 'rx2 coupe' 'rx-4' '626' 'glc' 'rx-7 gs' 'glc 4' 'glc custom l' 'glc custom' 'electra 225 custom' 'century luxus (sw)' 'century' 'skyhawk' 'opel isuzu deluxe' 'skylark' 'century special' 'regal sport coupe (turbo)' 'cougar' 'mirage' 'lancer' 'outlander' 'g4' 'mirage g4' 'montero' 'pajero' 'versa' 'gt-r' 'rogue' 'latio' 'titan' 'leaf' 'juke' 'note' 'clipper' 'nv200' 'dayz' 'fuga' 'otti' 'teana' 'kicks' '504' '304' '504 (sw)' '604sl' '505s turbo diesel' 'fury iii' 'cricket' 'satellite custom (sw)' 'fury gran sedan' 'valiant' 'duster' 'macan' 'panamera' 'cayenne' 'boxter' '12tl' '5 gtl' '99e' '99le' '99gle' 'dl' 'brz' 'baja' 'r1' 'r2' 'trezia' 'tribeca' 'corona mark ii' 'corona' 'corolla 1200' 'corona hardtop' 'corolla 1600 (sw)' 'carina' 'mark ii' 'corolla' 'corolla liftback' 'celica gt liftback' 'corolla tercel' 'corona liftback' 'starlet' 'tercel' 'cressida' 'celica gt' 'rabbit' '1131 deluxe sedan' 'model 111' 'type 3' '411 (sw)' 'super beetle' 'dasher' 'rabbit custom' '145e (sw)' '144ea' '244dl' '245' '264gl' 'diesel' '246']
Es ist grundsätzlich möglich, einzelne Features fachlich zu bewerten und numerisch zu kodieren, z. B. gas = 1 und diesel = 1.2.
z.B. die Anzahl von Tür und Zylinder wird bei der Datenbereinigung in Zahlen umgewandelt.
Die Durchführung einer solchen Bewertung setzt jedoch fundierte fachliche Kenntnisse voraus. Im Rahmen dieses Projekts ist dies nicht realistisch umzusetzen, da es den zeitlichen Rahmen deutlich überschreiten würde.
5.3 Modellauswahl für die Anwendung(Deployment)¶
Transformation wie log(), sqrt() oder x² können zwar zu einer verbesserten Modellgüte führen, ihre Wirkung ist jedoch stark von den Daten abhängig.¶
Für eine allgemeinere und praxisgerechte Anwendung habe ich daher das Verfahren Ordinal-Encoding + Ausreißerbehandlung gewählt.¶
6. Deployment (Anwendung der Ergebnisse)¶
6.1 Rahmenbedingungen für das Modelltraining und die Preisvorhersage¶
Das in Kapitel 4.4.3 vorgestellte Verfahren liefert die besten Ergebnisse und wird daher für das Modelltraining eingesetzt:
Kategorische Spalten als Ordinalwerte + numerische Spalten (standardisiert) + Ausreißerbehandlung → Lineares Modell
Die folgenden Überlegungen bilden die Grundlage des Modelltrainings:
- Die Daten umfassen verschiedene Autohersteller und -modelle. Die Preise liegen im Bereich von min. 5.118 € bis max. 45.400 €.
- Das Unternehmen (Auftraggeber) soll die Möglichkeit haben, ein Modell gezielt für ein bestimmtes Preissegment zu trainieren.
- Das Dashboard bietet die Option, ein gewünschtes Preissegment auszuwählen.
- Das Modell wird anschließend auf die Daten des ausgewählten Preisbereichs trainiert.
- Auf Basis des trainierten Modells kann das Unternehmen durch Eingabe bestimmter Parameter Preisvoraussagen durchführen.
Parameter für das Modelltraining:
encoding_mode: 2 fest
test_daten_size: 0.2 wählbar
squared_cols: 0 fest
Preisbereich: Auswahl durch Anwender wählbar
6.2 Dashborad für Modelltraining¶
Bedingungsanweisung für das Dashboard¶
Bedingungsanweisung für das Dashboard¶
Initialisierung beim Start
- Beim Starten des Dashboards wird das Modell mit den gesamten Daten trainiert.
- Folgende Darstellungen sind sichtbar:
- Histogramm der Preise
- Textfeld zur Anzeige der Modellergebnisse
- Histogramm der relativen Abweichungen (%) für die Testdaten
- Scatterplot der Preisdifferenzen für einzelne Werte
Auswahl des Preissegments
- Zwei Slider ermöglichen die Auswahl eines Preisbereichs.
- Beim Verschieben der Slider wird das Histogramm der ausgewählten Datensätze angezeigt.
- Das neue Histogramm wird über dem Original-Histogramm dargestellt, sodass der ausgewählte Bereich klar erkennbar ist.
Training mit ausgewählten Datensätzen
- Durch Drücken des Buttons „Start Training“ wird das Modell mit den ausgewählten Daten (Teil der Gesamtdaten) trainiert.
- Die Darstellungen (Histogramme, Scatterplot, Textfeld) werden nach dem Training aktualisiert.
Bewertung der Ergebnisse
- Die Qualität des Modells kann anhand der angezeigten Werte in den Diagrammen und Textfeldern bewertet werden.
#df= df.loc[df['highwaympg'] < limit_highwaympg_r] # df is cleaned for 'highwaympg'] < limit_highwaympg_r
# def training_carmodel(df_cp, categorical_columns, numerical_columns,encoding_mode=2, test_daten_size=0.2, squared_cols=0):
df= df.loc[df['highwaympg'] < 52]
#df.info()
start_price = float(df['price'].min())
end_price = float(df['price'].max())
print(type(start_price))
start_price, end_price
<class 'float'>
(5118.0, 45400.0)
#df.head(20)
#print("done")
import dash
from dash import Dash, dcc, html, Input, Output, callback
import plotly.express as px
import plotly.graph_objects as go
import json
import pandas as pd
global model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train
global df_train # copy of orig. df
global df_filtered # filtered df for ineractive modelltraining
row_count=203
df_train= df.copy(deep= True) # df_train is changed after Training
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = Dash(__name__, external_stylesheets=external_stylesheets)
styles = {
'pre': {
'border': 'thin lightgrey solid',
'overflowX': 'scroll'
}
}
slider_value_l=0
slider_value_r=50000
slider_trained_l=0
slider_trained_r=50000
# inital Modelltraining
# entfernen die Zeilen wo df['highwaympg'] >= limit_highwaympg_r] limit_highwaympg_r=52
model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train = training_carmodel(df_train,
categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0)
#show the relative deviation of the price : abs(price_diff) / price_orig *100: -> 'rel%': abs((y_test - y_pred)) / y_test * 100
# print("min.:",df_pred_train['rel%'].min(), "max.:",df_pred_train['rel%'].max(), "mean.:",df_pred_train['rel%'].mean())
my_outstr = (
f"r2_score: {r2_square_train:.4f}, "
f"mse: {mse:.4f}, "
f"min.%: {df_pred_train['rel%'].min():.4f}, "
f"max.%: {df_pred_train['rel%'].max():.4f}, "
f"mean%.: {df_pred_train['rel%'].mean():.4f}"
)
# show original price histogram: go.Histogram() create histogram
# size=300 ist the balkensize, 300 means 300$ for the Bin-width
# height=400 height in pixel, ähnlic wie 'height': '400px' in CSS. aber als string '400px' angegeben.
# for plotly: besser height=400
orig_hist = go.Histogram(
x=df['price'],
xbins=dict(
size=300 # Balkenabstand bzw. Bin-Größe
),
nbinsx=40,
opacity=0.5,
name='Original'
)
fig = go.Figure(orig_hist)
'''
fig.update_layout(
height=400, # Höhe in Pixel
width=800 # optional auch Breite
)
'''
app.layout = html.Div([
# Erst das Histogramm
html.Div(
dcc.Graph(id='compare_histogram', style={ 'width': '100%'}), # 'height':'500px',
style={'width': '100%', 'padding': '10px'}
),
html.Div(id='result_text', style={'whiteSpace': 'pre-line', 'padding': '10px', 'border': '1px solid #ccc'}),
# Dann das Slider-DIV darunter
html.Div([
#html.Label('Slider Links'),
dcc.Slider(
id='price_slider_l',
min=float(start_price),
max=float(end_price),
value=float(start_price),
step=300.0,
marks={i: str(i) for i in range(int(start_price), int(end_price)+1, 300)}
),
#html.Label('Slider Rechts'),
dcc.Slider(
id='price_slider_r',
min=float(start_price),
max=float(end_price),
value=float(end_price),
step=300.0, #300$ a step
marks={i: str(i) for i in range(int(start_price), int(end_price)+1, 300)}
)
], style={'width': '90%', 'padding': '10px'}),
# 2 elements in a DIV: button + histogram
html.Div([
html.Div([
html.Button('Start Training',
id='refresh_button',
n_clicks=0,
style={
'margin': '10px',
'height': '40px',
'border': '3px solid black', # <--- hier wird der Rahmen dicker
'borderRadius': '8px' # optional: abgerundete Ecken
}
),
dcc.Input(
id='output_trained',
type='text',
#placeholder='10000',
value=my_outstr,
style={'width': '100%'}
),
html.Div(id='alert_message',
style={
'color': 'red',
'margin': '10px',
'fontWeight': 'bold'
})
], style={'display': 'flex', 'alignItems': 'center'}),
html.Div([
dcc.Graph( id='rel_histogram',
figure=go.Figure( data=[go.Histogram(x= df_pred_train['rel%'])],
layout=go.Layout(height=400,
width=700,
title='Histogram: Die Häufigkeit der rel%-Abweichung',
xaxis_title='Relative Abweichung [%]',
yaxis_title='Häufigkeit'
)
)
),
dcc.Graph(id='scatter_plot',
figure=go.Figure(
data=[go.Scatter(
x=df_pred_train['Actual Value'],
y=df_pred_train['rel%'],
mode='markers',
opacity=0.7,
marker=dict(color='blue')
)],
layout=go.Layout(
height=400,
width=400,
title='Scatterplot: y_test(Preis in $) vs. rel% Abweichung',
xaxis_title='y_test (echte Werte in $)',
yaxis_title='Relative Abweichung [%]'
)
)
)
], style={'display': 'flex', 'alignItems': 'center'} )
], style={'width': '100%'})
] , style={'width': '90%', 'margin': 'auto'})
# check if enough rows exist
@app.callback(
Output('alert_message', 'children'),
Input('price_slider_l', 'value'),
Input('price_slider_r', 'value')
)
def validate_inputs(slider_value_l, slider_value_r):
df_train = df.copy(deep=True)
if slider_value_l > slider_value_r:
sl_val=slider_value_r
slider_value_r= slider_value_l
slider_value_l= sl_val
df_filtered = df_train[(df_train['price'] >= slider_value_l) & (df_train['price'] <= slider_value_r)]
row_count = len(df_filtered)
if row_count < 60:
return f"⚠️ Achtung: Gefilterte Datenmenge ist zu klein (nur {row_count} Zeilen, benötigt > 60)."
return ""
@app.callback(
Output('compare_histogram', 'figure'),
Output('result_text', 'children'),
Input('price_slider_l', 'value'),
Input('price_slider_r', 'value')
)
def update_compare_histogram(slider_value_l,slider_value_r):
# Ungefiltertes Histogramm
# slider_value_l: left value
# slider_value_r: right value
import plotly.graph_objects as go
global df_filtered, row_count
# why do the copy hier: df_filtered is a part of df_train. they have the same memory area.
# wenn f_filtered is changed, then memory of df_train is also changed even if df_train seem to be not be changed
df_train= df.copy(deep= True)
if slider_value_l > slider_value_r:
sl_val=slider_value_r
slider_value_r= slider_value_l
slider_value_l= sl_val
df_filtered = df_train[(df_train['price'] >= slider_value_l) & (df_train['price'] <= slider_value_r)]
# a0, a1, ...a29: 30 variable to be calculated, row_count should be 60 (48 for training, 12 for testing
row_count= len( df_filtered )
#print("row_count=", row_count)
# Gefiltertes Histogramm
filtered_hist = go.Histogram(
x=df_filtered['price'],
xbins=dict(
size=300 # Balkenabstand bzw. Bin-Größe
),
opacity=0.7,
name=f'Gefiltert ab {slider_value_l}'
)
# Beide Histogramme in einer Figure
fig = go.Figure([orig_hist, filtered_hist])
fig.update_layout(
barmode='overlay',
xaxis_title='Price',
yaxis_title='Count',
title='Distribution of Price (Vergleich Original & Gefiltert)',
height=450 #600
)
fig.update_xaxes(
tickmode='linear',
tick0=0,
dtick=2000
)
mystr = f"price_start: {slider_value_l}, price_end: {slider_value_r}, row_count: {row_count} ( should be > 60)"
return fig, mystr
@app.callback(
Output('rel_histogram', 'figure'),
Output('scatter_plot', 'figure'),
Output('output_trained', 'value'),
Input('refresh_button', 'n_clicks')
)
def update_rel_histogram(n_clicks):
import plotly.graph_objects as go
global model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train
global df_filtered
global slider_trained_l, slider_trained_r
# start the modeltraining
if n_clicks and n_clicks > 0 and row_count >=60 :
model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train = training_carmodel(
df_filtered, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0
)
# save the price range for price prediction
slider_trained_l= slider_value_l
slider_trained_r= slider_value_r
# print("r2_score:", r2_square_train, "mse:", mse, min.:",df_pred_train['rel%'].min(), "max.:",df_pred_train['rel%'].max(), "mean.:",df_pred_train['rel%'].mean())
my_outstr = (
f"r2_score: {r2_square_train:.4f}, "
f"mse: {mse:.4f}, "
f"min.%: {df_pred_train['rel%'].min():.4f}, "
f"max.%: {df_pred_train['rel%'].max():.4f}, "
f"mean%.: {df_pred_train['rel%'].mean():.4f}"
)
#print(my_outstr)
rel_hist = go.Histogram(
x= df_pred_train['rel%'],
xbins=dict(size=5),
nbinsx=100,
opacity=0.5,
name='relative in %'
)
fig = go.Figure(rel_hist)
#fig.update_layout(title='relative Abweichung')
fig.layout=go.Layout(height=400,
width=700,
title='Histogram: Die Häufigkeit der rel%-Abweichung',
xaxis_title='Relative Abweichung [%]',
yaxis_title='Häufigkeit'
)
#--------------------
scatter_plt = go.Scatter(
x=df_pred_train['Actual Value'],
y=df_pred_train['rel%'],
mode='markers',
opacity=0.7,
marker=dict(color='blue')
)
fig1 = go.Figure(scatter_plt)
fig1.update_layout(
title='Scatterplot: y_test(Preis in $) vs. rel% Abweichung',
xaxis_title='y_test (echte Werte in $)',
yaxis_title='Relative Abweichung [%]',
height=400,
width=700
)
#---------------------------
return fig, fig1, my_outstr
if __name__ == '__main__':
app.run(debug=True, port=8050)
6.3 Dashboard für Preisvoraussage(price prediction)¶
Bedingungsanweisung für das Dashboard¶
1. Textfeld: Preiseingabe¶
- Der Anwender gibt hier den gewünschten Preis ein.
- Basierend auf diesem Preis sucht das Dashboard den bestangepassten Datensatz aus den Trainingsdaten.
- Der gefundene Datensatz wird in den beiden Auto-Parameter-Fenstern angezeigt, sodass der Anwender alle Parameter einsehen kann.
- Parameter-Zuordnung:
horsepower→ Fenster „Motorleistung“enginesize→ Fenster „Motorgröße“
2. Textfeld: Motorleistung (horsepower)¶
- Motorleistung kann hier angepasst werden.
- Dieser Wert wird für die Preisvoraussage als veränderliche Größe verwendet.
3. Textfeld: Motorgröße (enginesize)¶
- Motorgröße kann hier angepasst werden.
- Dieser Wert wird für die Preisvoraussage als veränderliche Größe verwendet.
4. Textarea: Auto-Parameter¶
- Dient als Informationsfenster für den ausgewählten Datensatz.
- Alle Parameter des Datensatzes werden hier in zwei Bereichen angezeigt.
5. Button: Preis Berechnen¶
- Berechnet den Preis für die veränderten Parameter
horsepowerundenginesize.
6. Textfeld: Vorausgesagter Preis¶
- Zeigt den berechneten Preis für die aktuellen Parameter an.
7. Textfeld: Preis-Differenz¶
- Zeigt die Differenz zwischen dem vorausgesagten Preis und dem Preis des ausgewählten Datensatzes.
import dash
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
# Initialisiere die Dash-App
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
# Initialwerte
initial_value_3 = 0
initial_value_4 = 0
app.layout = dbc.Container([
html.H1("Dashboard für Preisvoraussage", className="my-4 text-center"),
dbc.Row([
# Spalte 1: Inputs
dbc.Col(dbc.Card([
dbc.CardHeader("Eingaben"),
dbc.CardBody([
html.Div([
html.Label("Preiseingeben"),
dcc.Input(
id='price_input',
type='text',
placeholder='10000',
value='10000',
style={'width': '100%'}
),
], className="mb-3"),
html.Div([
html.Label("Motorleistung"),
dcc.Input(
id='ps_value',
type='number',
value=initial_value_3,
style={'width': '100%'}
),
], className="mb-3"),
html.Div([
html.Label("Motorgröße"),
dcc.Input(
id='motor_size',
type='number',
value=initial_value_4,
style={'width': '100%'}
),
], className="mb-3"),
dbc.Button("Preis berechnen", id='action-button', n_clicks=0,
color="primary", className="w-100"),
])
]), width=3),
# Spalte 2: Parameter-Info: part 1
dbc.Col(dbc.Card([
dbc.CardHeader("Auto-Parameter"),
dbc.CardBody([
dcc.Textarea(
id='parameter_info',
readOnly=True,
value='',
style={
'width': '100%',
'height': '350px',
'backgroundColor': '#f0f0f0',
'resize': 'vertical'
}
),
])
]), width=3),
# Spalte 3: Parameter-Info: part 2
dbc.Col(dbc.Card([
dbc.CardHeader("Auto-Parameter"),
dbc.CardBody([
dcc.Textarea(
id='parameter_info1',
readOnly=True,
value='',
style={
'width': '100%',
'height': '350px',
'backgroundColor': '#f0f0f0',
'resize': 'vertical'
}
),
])
]), width=3),
# Spalte 4: Ergebnisse
dbc.Col(dbc.Card([
dbc.CardHeader("Ergebnisse"),
dbc.CardBody([
html.Div([
html.Label("Vorausgesagter Preis"),
dcc.Input(
id='price_pred',
type='text',
readOnly=True,
value='Warte auf Button-Klick',
style={'width': '100%', 'backgroundColor': '#f0f0f0'}
),
], className="mb-3"),
html.Div([
html.Label("Preis-Differenz"),
dcc.Input(
id='price_diff',
type='text',
readOnly=True,
value='Warte auf Button-Klick',
style={'width': '100%', 'backgroundColor': '#f0f0f0'}
),
]),
])
]), width=3),
]),
], fluid=True)
# Callback für Nr. 1 und Nr. 2
# Wenn Enter in input-1 gedrückt wird, wird der Wert in output-2 aktualisiert.
# price_input 1
# parameter_info 2
# ps_value 3
# motor_size 4
# action-button
# price_pred 5
# price_diff 6
@app.callback(
Output('parameter_info', 'value'),
Output('parameter_info1', 'value'),
Output('ps_value', 'value'),
Output('motor_size', 'value'),
Input('price_input', 'n_submit'),
State('price_input', 'value'),
#prevent_initial_call=True
)
def update_para_ps_engine_size(n_submit, price_input_value):
#slider_trained_l, slider_trained_r
global item_car_info
if not price_input_value:
return "Bitte Preis eingeben"
# use orig df, df_filtered_pred_new is no allowed to change
df_filtered_pred_new = df[(df['price'] >= slider_trained_l) & (df['price'] <= slider_trained_r)]
price_val = float(price_input_value) # string to float
len_pred_new = len(df_filtered_pred_new)
# search for item in df_filtered_pred_new
idx_nearest = None
price_nearest = None
# search for the nearst price index
idx_nearest = (df_filtered_pred_new['price'] - price_val).abs().idxmin()
price_nearest = df.loc[idx_nearest, 'price']
print(idx_nearest, price_nearest)
ps_val= df.loc[idx_nearest, 'horsepower']
motor_size_val= df.loc[idx_nearest, 'enginesize']
item_info_str = []
parts_str=[]
run_idx = 0
if idx_nearest is not None:
item_car_info = df_filtered_pred_new.loc[idx_nearest]
print("item_car_info: type", type(item_car_info))
for col, val in item_car_info.items():
item_info_str = '\n'.join(f"{col}: {val}" for col, val in item_car_info.items())
parts_str = item_info_str.split("enginetype", 1)
parts_str[1]='enginetype'+ parts_str[1]
#item_info_str = '\n'.join(f"{col}: {val}" for col, val in item_info.items())
return parts_str[0], parts_str[1], str(ps_val), str(motor_size_val)
# Callback für den Button und Felder 5 & 6
# Der Button löst eine Aktion aus, die die Werte in Feld 5 und 6 aktualisiert.
@app.callback(
Output('price_pred', 'value'),
Output('price_diff', 'value'),
Input('action-button', 'n_clicks'),
State('ps_value', 'value'), # Werte von input-3 und input-4 als State übergeben
State('motor_size', 'value'),
prevent_initial_call=True
)
def update_fields_on_button_click(n_clicks, value_3, value_4):
if n_clicks is None:
# Dies sollte wegen prevent_initial_call=True nicht direkt beim Start passieren
# Kann aber hilfreich sein, wenn der Button noch nicht geklickt wurde
return dash.no_update, dash.no_update
print("n_clicks", n_clicks)
# Hier definierst du die Logik für die Aktualisierung der Felder 5 und 6
# Basierend auf den Werten aus Feld 3 und 4
try:
val3 = float(value_3)
val4 = float(value_4)
except (ValueError, TypeError):
return "Ungültiger Wert", "Ungültiger Wert"
item_car_info['horsepower'] = val3
item_car_info['enginesize'] = val4
print(item_car_info['horsepower'])
print(item_car_info['enginesize'])
#model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train = training_carmodel(
# df_filtered, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0
df_item_car_info = pd.DataFrame([item_car_info]) # df_item_car_info: as a DataFrame
price_before= item_car_info['price']
price_predicted= predict_price(df_item_car_info, model_train, label_encoders_train, scaler_train,
categorical_columns, numerical_columns,squared_cols=0)
print("price_pred_str:", price_predicted)
print("price_before:", price_before)
price_pred_dff= price_predicted[0] - price_before
price_predicted_str= str(int(price_predicted[0]))
price_pred_dff_str= str(int(price_pred_dff))
return price_predicted_str, price_pred_dff_str
# Run the app
if __name__ == '__main__':
app.run(debug=True, port=8051)
188 9995.0 item_car_info: type <class 'pandas.core.series.Series'>
Fazit und Ausblick¶
1. Projektstruktur nach CRISP-DM¶
Die Projektarbeit wurde vollständig nach dem CRISP-DM-Schema durchgeführt. Sowohl deskriptive Analysen als auch explorative Datenanalysen (EDA) wurden systematisch umgesetzt.
2. Erste Datenübersicht¶
Nach dem Einlesen der Daten wurden gängige Methoden wie df.info(), df.describe(), df.nunique(), df.isna().sum() und df.head() angewendet, um einen ersten Überblick über die Daten zu erhalten.
3. Deskriptive statistische Analyse¶
Anschließend wurden die Daten deskriptiv statistisch analysiert, um die Eigenschaften der Daten noch besser zu verstehen.
4. Explorative Datenanalyse (EDA)¶
EDA wurde eingesetzt, um Muster und Zusammenhänge innerhalb der Daten zu erkennen.
5. Datenaufbereitung¶
Nach der statistischen Untersuchung erfolgte die Datenaufbereitung. Da der Datensatz mit 205 Zeilen und 26 Spalten relativ klein ist, war die Datenbereinigung minimal:
- Zerlegung von Spalten
- Umwandlung kategorialer Daten in numerische Formate
- Behandlung von Ausreißern
6. Maschinelles Lernen¶
Der Schwerpunkt der Arbeit liegt eindeutig im Bereich Maschinelles Lernen:
- Verschiedene Verfahren für Encoding wurden untersucht und ausführlich diskutiert.
- Lineare Regression wurde für das Modelltraining verwendet.
- Es wurde getestet, ob nicht-lineare Transformationen numerischer Spalten die Modellleistung verbessern.
- Überraschenderweise erzielte die
sqrt(x)-Transformation die besten Ergebnisse.
- Überraschenderweise erzielte die
- Es wurde gezeigt, dass Label-Encoding bessere Ergebnisse liefert als One-Hot-Encoding, was im Dokument detailliert begründet wird.
- Durch die Behandlung von Ausreißern verbesserte sich die Modellgenauigkeit um etwa 1%.
- Insgesamt erreicht das Modell einen Score von 90 % (1 = perfekt). Für die Preisvorhersage wurde Label-Encoding verwendet.
7. Dashboard: Modelltraining¶
Am Ende wurden zwei Dashboards erstellt, die funktional und optisch ansprechend sind:
- Über das Dashboard für das Modelltraining kann ein beliebiges Preissegment ausgewählt und trainiert werden.
- Einschränkung: Der Preisbereich muss ausreichend groß sein, damit genügend Daten für ein sinnvolles Modelltraining vorhanden sind.
8. Dashboard: Preisvorhersage¶
- Nach dem Training kann die Preisvorhersage direkt über das Dashboard durchgeführt werden.
- Ein Referenzpreis wird eingegeben, und über zwei Parameter können Varianten des Autos verändert werden.
- Das Modell prognostiziert daraufhin den Preis für die veränderte Variante.
- Tests zeigen, dass das Verfahren auch mit nur 205 Datenzeilen gut funktioniert.
- Mit größeren Datensätzen könnte die Modellqualität weiter verbessert werden.
9. Ausblick¶
- Die Arbeit zeigt, dass KI ein wertvolles Werkzeug ist, das die Analyse und Modellierung wesentlich unterstützt.
- Zukünftig könnten weitere Features, größere Datensätze oder komplexere Modelle die Vorhersagegenauigkeit weiter erhöhen.